Learn Spring Security OAuth: The Certification Class - Part 3

Lesson 1: New OAuth2 Social Login (text-only)

1. Goal

In this lesson, we’ll learn about authenticating our apps using a social login provider, the benefits of using social login providers and how to integrate with them.

2. Lesson Notes

The relevant module you need to import when you’re starting with this lesson is: lsso-module4/oauth2-social-login-start

If you want have a look at the fully implemented lesson, as a reference, feel free to import: lsso-module4/oauth2-social-login-end

2.1. Social Login and its Benefits

Social login means authenticating users for our app by delegating the authentication to social login providers such as Github, Google, Facebook etc.

Using social login providers for our app gives a lot of ease and flexibility to our users as they don’t have to create a new set of credentials to login to our app, and instead reuse the credentials already created at a trusted provider.

It also makes our job easier as it gives us an opportunity to delegate the authentication responsibility to the social login provider.

2.2. Spring Security Support for Social Login

The Spring Security OAuth2 framework provides very mature support for login with popular social login providers.

For Common OAuth2 providers such Google, Github, Facebook etc, it provides ready to use inbuilt configurations.

Furthermore, the framework is flexible and customizable if needed for a custom provider.

2.3. Authentication with GitHub

In our example, we’re going to authenticate our app using a popular social login provider - GitHub.

We’ll also try to access secured resources from GitHub for the authenticated user, namely the user’s profile and repository information.

Of course, the same functionality can be extended to any social login provider.

2.4. Configuring Security to Authenticate with OAuth2

In the Client module, we’ve included the spring-boot-starter-oauth2-client dependency . This brings in all the OAuth2 security and client libraries that we need.

Next, we need to configure security to enable OAuth2 authentication for our app.

Let’s open the class ClientSecurityConfig and make the required changes:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .anyRequest().authenticated()
        .and()
        .oauth2Login()
        .and()
        .logout().logoutSuccessUrl("/");
}

Here, in the configure method, we’ve enabled OAuth2 login by using the DSL oauth2Login().

We’ve also configured the logout URL so that we can successfully log out from the app by using the logout() DSL.

These configurations will ensure that, for authentication, we’re redirected to the social login provider that we’ll register in our app in the next step.

But, before that, we have to understand the setup we need to do at the provider’s end.

2.5. Creating an OAuth App at the Provider’s Platform

To leverage any social provider to authenticate in our app using OAuth2, we first need to register an OAuth2 app at the provider’s platform.

For Github, this is done using the steps mentioned here: https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-an-oauth-app

One important configuration that needs to be done at the provider’s end is configuring the correct callback URL.

A callback URL is a URL that the provider will redirect to after successful authentication.

This is defined and configured automatically by the framework.

Let’s see this for the app registered for our example:

For our app, the URL will be: http://localhost:8082/lsso-client/login/oauth2/code/github

Once an app is successfully registered with a provider, this creates a unique client-id and client-secret to identify the app. We can see these properties on the registered app as well.

2.6. Configuring the Registered OAuth2 app in our Client

Next, we need to add the client credentials we got from the provider in our Client module.

This configuration is similar to the one we would have for a local provider.

Let’s open the application.yml and add the details:

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: github-client-id
            client-secret: github-client-secret

Here, we’ve to register with this provider using its name and the client-id and client-secret of the App that we will create at the providers’ end.

Note, please enter here the Client Id and Client Secret of the App that you will register at Github.

This completes our Client registration, though, it’s interesting to know here that due to Spring Security support for popular OAuth2 providers such as GitHub, we were able to register our Client by using only the client name and the above 2 properties.

All this is possible because the rest of the properties are already defined in the class CommonOAuth2Provider :

GITHUB {

	@Override
	public Builder getBuilder(String registrationId) {
		ClientRegistration.Builder builder = getBuilder(registrationId,
				ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
		builder.scope("read:user");
		builder.authorizationUri("https://github.com/login/oauth/authorize");
		builder.tokenUri("https://github.com/login/oauth/access_token");
		builder.userInfoUri("https://api.github.com/user");
		builder.userNameAttributeName("id");
		builder.clientName("GitHub");
		return builder;
	}
}

For e.g. for Github, we already have the properties above defined.

To leverage these predefined configurations, it’s important that our client name registered in application.yml matches with the enumerations provided in this class.

Next, we need to add a Welcome Screen for the user and a controller that renders this screen.

2.7. Adding a New Welcome Screen

We’ve added a welcome page user.html in the start point of the lesson that contains the basic structure.

Let’s open user.html and add a simple welcome text:

<div class="container">
	<!-- Welcome Text -->    
	<h1>Welcome!!</h1>
	<div class="row">	

Our welcome screen is ready.

Next, let’s add a simple controller to render this:

@Controller
public class UserProfileController {

    @GetMapping("/user")
    public String user(Model model) {
        return "user";
    }

}

Next, let’s run the application and navigate to http://localhost:8082/lsso-client/. This should render the login screen.

If we click the login button, we can see it redirects us to the GitHub login form; this means that we’ve successfully integrated our app with the GitHub login.

After entering our correct GitHub credentials, we’re redirected to the Welcome screen.

Now we are able to authenticate using GitHub.

This functionality is even more powerful if we can access secured GitHub resources of the logged-in user such as his profile, basic information, repositories etc.

Let’s try this next.

2.8. Accessing Secured GitHub Resources

As soon as a user is authenticated, Spring Security wraps the basic user details provided by the social provider in an OAuth2User object.

This object is available as an authentication principal in all of our web requests.

Let’s leverage this principal object to access basic user details.

We’ll modify our existing user() method to use this principal object:

@Controller
public class UserProfileController {

    @GetMapping("/user")
    public String user(Model model, @AuthenticationPrincipal OAuth2User principal) {
        return "user";
    }

}

Next, we’ll extract a couple of details from the principal object and add them as model attributes to be displayed on the Welcome screen.

The attributes that we will extract are returned from GitHub for the authenticated user.

We can find the complete list of attributes in the GitHub documentation - https://docs.github.com/en/free-pro-team@latest/rest/reference/users#get-the-authenticated-user

@Controller
public class UserProfileController {

    @GetMapping("/user")
    public String user(Model model, @AuthenticationPrincipal OAuth2User principal) {
        model.addAttribute("name", principal.getAttribute("name"));
        model.addAttribute("id", principal.getAttribute("login"));
        model.addAttribute("img", principal.getAttribute("avatar_url"));
        return "user";
    }

}

Next, let’s render these details on the welcome screen:

<h1>Welcome!!</h1>
<div class="row">
    <div class="col-sm-2">
        <div>
            <!-- Profile Image -->                
            <img th:src="@{${img}}" class="img-thumbnail" width="100" height="100"/>
        </div>
    </div>
    <div class="col">
    <!-- Basic Attributes -->            
        <div th:text="${name}"/>
        <div th:text="${id}"/>
    </div>
</div>

In the user.html file above, we’ve displayed the user’s image in the img tag, and just next to the image we displayed the user’s basic details - name and uid.

Now, our basic profile page is ready; let’s reboot the application and test this feature.

As we login again we can see, we can access and display basic user details.

Until now, we’ve leveraged only the basic detail that we get in the auth response from GitHub.

The next step is to understand how we can access secured resources such as GitHub user repositories which are not included in the basic details response.

2.9. Accessing Secured API Endpoints from our App

To access APIs provided by GitHub, we’ll use the WebClient.

But we also need to ensure that the access token we’ve received from GitHub is sent with any secured API calls we make. For this, we need a WebClient bean.

Naturally, the configuration is the same as we would have when working with a local provider.

This bean is already present in the start point; let’s just enable it by uncommenting the @Bean annotation:

@Bean
WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, authorizedClientRepository);
    oauth2.setDefaultOAuth2AuthorizedClient(true);
    return WebClient.builder()
        .apply(oauth2.oauth2Configuration())
        .build();
}

Next, in our UserProfileController we’ll inject the WebClient and use this to invoke the repositories URL. This URL is present in the principal itself.

The final state of our controller should look like this:

@Controller
public class UserProfileController {

    @Autowired
    private WebClient webClient;

    @GetMapping("/user")
    public String user(Model model, @AuthenticationPrincipal OAuth2User principal) {
        model.addAttribute("name", principal.getAttribute("name"));
        model.addAttribute("id", principal.getAttribute("login"));
        model.addAttribute("img", principal.getAttribute("avatar_url"));

        String repoUrl = principal.getAttribute("repos_url");

        List<Map<Object, Object>> repos = this.webClient.get()
            .uri(repoUrl)
            .retrieve()
            .bodyToMono(new ParameterizedTypeReference<List<Map<Object, Object>>>() {
            })
            .block();

        model.addAttribute("repos", repos);
        return "user";
    }

}

Next, we’ll display the information about repositories by iterating through all the repos to display the names:

<div>
<!-- Repos -->                
    <h4>User Repos</h4>                
    <div th:each="repo : ${repos}">
        <div th:text="${repo.name}"/>
    </div>
</div>

This brings us to the final state of our application.

As we login again, we can see all the user’s public repos are visible.

This means that:

  • we were able to successfully login with GitHub
  • we were able to access user’s personal details and further
  • we were also able to access a secured resource - in this case, the user’s repository list

3. Resources

- Spring Security 5 – OAuth2 Login

- Spring Security Reference - OAuth 2.0 Client

- Next Generation OAuth Support with Spring Security 5.0

Lesson 2: Refreshing a Token (text-only)

1. Goal

In this lesson, we’ll learn about Refresh Tokens, how to work with them and what their benefits are.

2. Lesson Notes

The relevant module you need to import when you’re working with this lesson is: lsso-module4/refresh-token

This lesson only needs a single reference codebase, so there is no end version of the project.

We’ll also need to import the Postman collection that contains all the REST calls that we need to run for this lesson.

2.1. Refresh Tokens

Refresh Tokens are credentials used to obtain Access Tokens, either when the current Access Token expires or to obtain an Access Token with fewer scopes.

The main benefit of a Refresh Token is that the user can obtain a new Access Token without re-authenticating.

Refresh Tokens are often long-lived, but they can be invalidated by the Authorization Server.

Because of their long lifetime, Refresh Tokens must be stored securely to prevent being leaked.

2.2. Refreshing the Token Using Postman

Before diving into the code, we’ll carry out the refresh process manually using Postman.

First, we’ll need the Authorization Server and Resource Server to be running.

Then, we’ll execute the following requests consecutively to obtain a new access token:

  • GET - Extract Authorization Endpoint - extracts the authorization endpoint
  • POST - Request Authorization Code - gets the Authorization Code from the Authorization Server
  • POST - Request Access Token - exchanges the Authorization Code retrieved above to get an Access Token

Note that the Access Token response that we get in the last call also includes a refresh_token value:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlUktVWG10TFhKMHBBNkxBS29aWko1ZlU0VDhCdmxKdERCb3pXanFFdnhjIn0.eyJqdGkiOiIyYzBhZDUwZi0xN2E2LTRkYTYtYmViMy05MGRlMmJkNjkyMzkiLCJleHAiOjE1ODY2MjU0NjcsIm5iZiI6MCwiaWF0IjoxNTg2NjI1MTY3LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODMvYXV0aC9yZWFsbXMvYmFlbGR1bmciLCJzdWIiOiJhNTQ2MTQ3MC0zM2ViLTRiMmQtODJkNC1iMDQ4NGU5NmFkN2YiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJsc3NvQ2xpZW50IiwiYXV0aF90aW1lIjoxNTg2NjI1MTU0LCJzZXNzaW9uX3N0YXRlIjoiZGUyMzI1YjUtMDhmZS00ZDhjLTkwYzYtYTM2ZGI0ZTRlZGFlIiwiYWNyIjoiMSIsInNjb3BlIjoicHJvZmlsZSByZWFkIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiam9obkB0ZXN0LmNvbSJ9.cx_IRFYhBgPfBg184-ZHMU3E8Xhq9ePBU8lZty90PM5I6PPbeS7eNyzDeHExeq8jCDFHu7A46UGHROAwhQToEBa9Ga_ZSlvJNYzy0DSsMfHMNvNLIBs-iwnY2YXhHPvHIq8jBe0SMQcZ7j7cWp3jNC0HMaN3j0yKicxLKf_bgXH7vLy_06CpTzB76Z_3yLsU5ngVmlwUbSuoo9weO11KB3IINgsWDBIpQ9TaehhvEEgWM9AY0BFl8SXFSTOwM8_EE-k8IUqQvWaH8PtFETggcnUUDs2bz2o6vNsb0Eqsr3fmhq6bBPcSIjXiiPI4hS7EdHubUqpdTxhisiIZdiO7ng",
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwZDkwY2JkNy03MTY0LTQyY2MtODhlMi1kMjE1ZTc5YWU4ZWEifQ.eyJqdGkiOiI0ZWI1YTg1MC1kZTZmLTQzYWUtYjMxNi1jYzg5MDE0OWM3M2UiLCJleHAiOjE1ODY2MjY5NjcsIm5iZiI6MCwiaWF0IjoxNTg2NjI1MTY3LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODMvYXV0aC9yZWFsbXMvYmFlbGR1bmciLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODMvYXV0aC9yZWFsbXMvYmFlbGR1bmciLCJzdWIiOiJhNTQ2MTQ3MC0zM2ViLTRiMmQtODJkNC1iMDQ4NGU5NmFkN2YiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibHNzb0NsaWVudCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImRlMjMyNWI1LTA4ZmUtNGQ4Yy05MGM2LWEzNmRiNGU0ZWRhZSIsInNjb3BlIjoicHJvZmlsZSByZWFkIn0.TvfG5uf0xGbnQLOnLdLbRn5ut0duANQQkSTIIXEWiqs",
    "token_type": "bearer",
    "not-before-policy": 0,
    "session_state": "de2325b5-08fe-4d8c-90c6-a36db4e4edae",
    "scope": "profile read"
}

The Refresh Token can also have a validity time after which it cannot be used anymore to obtain a new set of credentials from the Authorization Server. This validity time is typically longer than the Access Token lifetime.

Here, this is expressed in the response by the refresh_expires_in property. However, this property is not standardized and can have a different name or not be present at all for other Authorization Servers.

2.3. Retrieving a New Access Token Using the Refresh Token

After obtaining the Refresh Token, we’ll use it to receive a new set of credentials by calling the Token Endpoint.

In Postman, open the request:

POST - Refresh Token

We’ll set the grant_type to refresh_token and the refresh_token parameter to the refresh_token value we got in the previous request:

grant_type:refresh_token
client_id:lssoClient
client_secret:lssoSecret
refresh_token:eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwZDkwY2JkNy03MTY0LTQyY2MtODhlMi1kMjE1ZTc5YWU4ZWEifQ.eyJqdGkiOiI0ZWI1YTg1MC1kZTZmLTQzYWUtYjMxNi1jYzg5MDE0OWM3M2UiLCJleHAiOjE1ODY2MjY5NjcsIm5iZiI6MCwiaWF0IjoxNTg2NjI1MTY3LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODMvYXV0aC9yZWFsbXMvYmFlbGR1bmciLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODMvYXV0aC9yZWFsbXMvYmFlbGR1bmciLCJzdWIiOiJhNTQ2MTQ3MC0zM2ViLTRiMmQtODJkNC1iMDQ4NGU5NmFkN2YiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibHNzb0NsaWVudCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImRlMjMyNWI1LTA4ZmUtNGQ4Yy05MGM2LWEzNmRiNGU0ZWRhZSIsInNjb3BlIjoicHJvZmlsZSByZWFkIn0.TvfG5uf0xGbnQLOnLdLbRn5ut0duANQQkSTIIXEWiqs

Executing this request we get a new Access Token and Refresh Token in the response:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlUktVWG10TFhKMHBBNkxBS29aWko1ZlU0VDhCdmxKdERCb3pXanFFdnhjIn0.eyJqdGkiOiI2Y2Y5YzdhMy1lMjg3LTRmZDEtYjhmMy0wNjgzZTRmNDMzYzMiLCJleHAiOjE1ODY2MjYyMjQsIm5iZiI6MCwiaWF0IjoxNTg2NjI1OTI0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODMvYXV0aC9yZWFsbXMvYmFlbGR1bmciLCJzdWIiOiJhNTQ2MTQ3MC0zM2ViLTRiMmQtODJkNC1iMDQ4NGU5NmFkN2YiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJsc3NvQ2xpZW50IiwiYXV0aF90aW1lIjoxNTg2NjI1MTU0LCJzZXNzaW9uX3N0YXRlIjoiZGUyMzI1YjUtMDhmZS00ZDhjLTkwYzYtYTM2ZGI0ZTRlZGFlIiwiYWNyIjoiMSIsInNjb3BlIjoicHJvZmlsZSByZWFkIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiam9obkB0ZXN0LmNvbSJ9.G3_m9ZccEEf-MXmE7TfPkwQJ75j58oQ24fG8j5a1y8Y2kNnHQm2mBP8RgXocii2GvMxzJZp-tWbJN427Mq_fFqJIYwbFjiW2RB7DWE1X4itpPoNiUVkGnr7MaGo9ZeGzbdOIn-Q48FJrlHXXNjir7sji1tJpheVThAueJxW7JlAmWT3kW_-kf7npiosUO5ZtHDM1H5bKX8u1IzsOnfzlUteMM-aAw904fZQOIoVdrjLEeDVCYCyt3iueCgh_MCjFPITL3ewLu2PaBgf4le0PONmMExqtzWL3LHywKHCrg2Al4kh7fvuIGowM_Rrcq8-KPhhOLXcUM6Qbp5FIepNA5w",
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwZDkwY2JkNy03MTY0LTQyY2MtODhlMi1kMjE1ZTc5YWU4ZWEifQ.eyJqdGkiOiJkZDNkNDg5OC1lMjAzLTQyNWItYTBjMi02ZGVmMGE5NTFjY2EiLCJleHAiOjE1ODY2Mjc3MjQsIm5iZiI6MCwiaWF0IjoxNTg2NjI1OTI0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODMvYXV0aC9yZWFsbXMvYmFlbGR1bmciLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODMvYXV0aC9yZWFsbXMvYmFlbGR1bmciLCJzdWIiOiJhNTQ2MTQ3MC0zM2ViLTRiMmQtODJkNC1iMDQ4NGU5NmFkN2YiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibHNzb0NsaWVudCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImRlMjMyNWI1LTA4ZmUtNGQ4Yy05MGM2LWEzNmRiNGU0ZWRhZSIsInNjb3BlIjoicHJvZmlsZSByZWFkIn0.dsFzvpUtawa3CEVeg-p2x3bgOBGIlPvWVKBR337KUZc",
    "token_type": "bearer",
    "not-before-policy": 0,
    "session_state": "de2325b5-08fe-4d8c-90c6-a36db4e4edae",
    "scope": "profile read"
}

Now that we’ve seen the sequence of calls let’s have a look at the code.

2.4. Client Configuration

Let’s move on to the Client app.

Here, we’re using the spring-boot-starter-oauth2-client library which includes the full Refresh Token support along with core Spring Security libraries.

OAuth 2.0 Client support integrates well with the new WebClient which will be replacing the classic RestTemplate.

Let’s have a deeper look at the WebClient support which can refresh tokens automatically.

In ClientSecurityConfig in the client module, we’ve implemented the WebClient bean:

@Bean
WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, authorizedClientRepository);
    oauth2.setDefaultOAuth2AuthorizedClient(true);
    return WebClient.builder()
        .apply(oauth2.oauth2Configuration())
        .build();
}

As we’ve previously seen, OAuth 2.0 Client support integrates well with WebClient using the ExchangeFilterFunction. This function provides a simple mechanism for requesting protected resources by including the retrieved Access Token as a Bearer Token in the requests.

Also, the WebClient will automatically refresh the Access Token if it’s expired and a Refresh Token is available.

2.5. Debugging the WebClient

Let’s open RefreshTokenOAuth2AuthorizedClientProvider that contains the logic of refreshing the Access Token using a Refresh Token.

We’ll start by placing a breakpoint in the authorize() method where the re-authorization logic is found.

When sending requests with WebClient, the ExchangeFilterFunction will call this authorize() method which will check if the Refresh Token is available and whether the Access Token has expired.

Let’s run the Client app in debug mode, open http://localhost:8082/lsso-client/ and login with credentials: john@test.com/123

Upon successful login:

  • we’re redirected to the /projects uri
  • this triggers our getProjects() method in the ProjectClientController
  • which further uses the WebClient to request a resource from the Resource Server

The debugger will stop at this code block:

OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (authorizedClient == null ||
		authorizedClient.getRefreshToken() == null ||
		!hasTokenExpired(authorizedClient.getAccessToken())) {
	return null;
}

As we step into the code block we can see that the refreshToken attribute is present and the Access Token hasn’t expired because we just obtained it.

As we proceed further we can see that the process returns null from this provider and the logic of refreshing the token will not be executed.

To see the Refresh Token in action we’ll have to wait for the Access Token to expire.

Let’s wait for 5 minutes for the Access Token to expire and then let’s reload the page to trigger the complete flow again.

The debugger will stop at the previous code block, but now since our Access Token has expired we can proceed to the next code block:

OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(
		authorizedClient.getClientRegistration(), authorizedClient.getAccessToken(),
		authorizedClient.getRefreshToken(), scopes);
OAuth2AccessTokenResponse tokenResponse =
		this.accessTokenResponseClient.getTokenResponse(refreshTokenGrantRequest);

Up until this point the execution obtains the scopes that were previously specified in the request and constructs the parameters needed for refreshing the token.

Primarily, it creates a refreshTokenGrantRequest - from the existing Access and Refresh Tokens, and finally fetches a new Access Token by executing this request.

The new Access Token should be present at tokenResponse > accessToken.

We can also compare it with the previous Access Token value which is present at authorizedClient > accessToken > tokenValue.

2.6. Authorization Server Configuration

As we’ve mentioned, Authorization Servers provide support for configuring Refresh Token properties.

This includes the validity time and how many times they can be used to obtain a new Access Token.

Since this property is not standardized, Spring doesn’t populate the expiresAt field with the value from the Authorization Server by default, so here it will be null.

Since we’re using Keycloak, let’s have a look at some options we have for configuring Refresh Tokens in the Keycloak Admin console.

Open - Realm Settings -> Tokens tab

The first property we’ll look at is the Revoke Refresh Token. If enabled, a Refresh Token can only be used up to Refresh Token Max Reuse and is revoked when a different token is used.

If disabled, Refresh Tokens are not revoked and can be used multiple times

We can also configure the SSO Session Idle which controls the Refresh Token lifetime.

3. Resources

- Understanding Refresh Tokens - Auth0

- RFC6749 - Refresh Token

- Spring Security Reference - OAuth 2.0 Client

- Spring 5 WebClient

- Keycloak - Session and Token timeouts

Lesson 3: Testing OAuth2 Clients (text-only)

1. Goal

In this lesson, we’ll learn about the support that Spring Security provides for testing OAuth2 Clients.

2. Lesson Notes

The relevant module you need to import when you’re starting with this lesson is: lsso-module4/testing-oauth2-clients-start

If you want have a look at the fully implemented lesson, as a reference, feel free to import: lsso-module4/testing-oauth2-clients-end

2.1. Overview

Spring Security provides solid support for testing OAuth2 Clients.

We can configure an OAuth2 Authorized Client in our tests or configure Token attributes such as scopes to test our Client endpoints.

To use the oauth2Client() API, we need to add the spring-security-test dependency in our client app:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

2.2. Mocking an Authorized Client

Let’s first create an endpoint which will display a simple message and expects an authorized “custom” client:

@Controller
public class ProjectClientController {

    // ...
	
    @ResponseBody
    @GetMapping("/profile-simple")
    public String getUserProfileInfo(@RegisteredOAuth2AuthorizedClient("custom") OAuth2AuthorizedClient authorizedClient) {
        return "Your user profile";
    }

}

Spring can resolve the OAuth2AuthorizedClient using @RegisteredOAuth2AuthorizedClient.

Now that we’ve created this endpoint, let’s add an integration test for it.

In our OAuth2ClientIntegrationTest class, let’s add the necessary annotations to test our controller and autowire the MockMvc :slight_smile:

@AutoConfigureMockMvc
public class OAuth2ClientIntegrationTest {

    @Autowired
    private MockMvc mvc;

    // ...
}

We will start by writing a test which simply calls the endpoint without an active OAuth client:

@Test
public void givenSecuredEndpoint_whenCallingEndpoint_thenSuccess() throws Exception {
   this.mvc.perform(get("/profile-simple"))
       .andExpect(status().is3xxRedirection());
}

Notice that we’re expecting back a redirection because, naturally, without a client we’ll be redirected to authenticate on the server side.

The OAuth2AuthorizedClientManager which is responsible for the management of OAuth2AuthorizedClient(s) tries to resolve the authorized Client required by the endpoint.

But since this isn’t present in the request, we get redirected to the authorization URI of our OAuth2 Authorization Server that is defined in our application.properties .

Now let’s do the same thing but this time use the oauth2Client() when configuring the request and mock our “custom” client :slight_smile:

@Test
public void givenSecuredEndpoint_whenCallingEndpoint_thenSuccess() throws Exception {
    // ...

    this.mvc.perform(get("/profile-simple").with(oauth2Client("custom")))
        .andExpect(status().isOk());
}

When we run the test, we can see that it passes this time because our mocked OAuth2AuthorizedClient was present in the request.

2.3. Testing Scopes

Now let’s see how we can test the OAuth2 Access Token scopes.

If our controller inspects scopes then we can configure them using the accessToken() method.

We’ll add a simple endpoint which will inspect the scopes and, based on them, return different responses:

@ResponseBody
@GetMapping("/profile")
public String getUserProfileInfoWithScopes(@RegisteredOAuth2AuthorizedClient("custom") OAuth2AuthorizedClient authorizedClient) {
    Set<String> scopes = authorizedClient.getAccessToken()
        .getScopes();
    if (scopes.contains("admin.users:read")) {
        return "All users";
    } else if (scopes.contains("users:read")) {
        return "Your user profile";
    } else {
        throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Forbidden.");
    }
}

Notice we’re retrieving the scopes from the Access Token. For the admin.users.read and users:read scopes, we’re returning different messages. and for any other scope, a 403 Forbidden Response .

Now, that our endpoint is ready, let’s write a test mocking these scopes.

We’ll start by testing the admin.users:read scope:

@Test
public void givenOauth2Client_whenUsingScopes_thenSuccess() throws Exception {
    this.mvc.perform(get("/profile").with(oauth2Client("custom")
        .accessToken(new OAuth2AccessToken(BEARER, "token", null, Instant.now(), Collections.singleton("admin.users:read")))))
        .andExpect(content().string("All users"))
        .andExpect(status().isOk());
}

Here, we’ve configured the Client using oauth2Client() API as before, then used the accessToken() method to configure a token with the admin.users:read scope , and verified the expected result.

In the same method, we’ll test the users:read scope using the same approach:

@Test
public void givenOauth2Client_whenUsingScopes_thenSuccess() throws Exception {
    // ...

    this.mvc.perform(get("/profile").with(oauth2Client("custom")
        .accessToken(new OAuth2AccessToken(BEARER, "token", null, Instant.now(), Collections.singleton("users:read")))))
        .andExpect(content().string("Your user profile"))
        .andExpect(status().isOk());
}

and finally, let’s test it without providing any scopes:

@Test
public void givenOauth2Client_whenUsingScopes_thenSuccess() throws Exception {
    // ...

    this.mvc.perform(get("/profile").with(oauth2Client("custom")))
        .andExpect(status().isForbidden());
}

As you can see, we called the endpoint 3 times, each time with different scopes. We can then run the test to verify all 3 assertions pass.

2.4. Testing the Principal Name

Similar to the scopes, if the controller inspects the Resource Owner name, then we can configure it using principalName() .

Again, let’s write a simple endpoint which will permit only the Resource Owner whose name ends with “@baeldung.com”.

We’ll add a similar method as before, that retrieves the Authorized Client, then check the principalName and return a welcome text if it matches the pattern or else return Forbidden status:

@ResponseBody
@GetMapping("/principal-name")
public String getPrincipalName(@RegisteredOAuth2AuthorizedClient("custom") OAuth2AuthorizedClient authorizedClient) {
    if (authorizedClient.getPrincipalName()
        .endsWith("@baeldung.com")) {
        return "Welcome admin";
    } else {
        throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Forbidden.");
    }
}

Next, let’s write the test for the above endpoint.

We start by testing the Resource Owner whose name ends with “@baeldung.com”:

public class OAuth2ClientIntegrationTest {

    // ...

    @Test
    public void givenOauth2Client_whenSetPrincipalName_thenSuccess() throws Exception {
        this.mvc.perform(get("/principal-name").with(oauth2Client("custom").principalName("admin@baeldung.com")))
            .andExpect(content().string("Welcome admin"))
            .andExpect(status().isOk());
    }

}

Next, let’s add another test in the same method for a Resource Owner whose name ends with “@gmail.com”:

@Test
public void givenOauth2Client_whenSetPrincipalName_thenSuccess() throws Exception {
    // ...

    this.mvc.perform(get("/principal-name").with(oauth2Client("custom").principalName("user@gmail.com")))
        .andExpect(status().isForbidden());
}

As we run the test we can verify that it passes successfully.

2.5. Using a Real Client Registration

This is useful if we want to test our controllers using a real ClientRegistration from our properties file.

To retrieve our Client configuration we need to autowire the ClientRegistrationRepository in our OAuth2ClientIntegrationTest:

public class OAuth2ClientIntegrationTest {
    
	// ...
	
    @Test
    public void givenClient_whenGetRegistration_thenSuccess() throws Exception {
    }
}

Next, we’ll retrieve the clientRegistration by id and verify its properties:

@Test
public void givenClient_whenGetRegistration_thenSuccess() {
    ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("custom");
    assertThat(clientRegistration.getRegistrationId()).isEqualTo("custom");
    assertThat(clientRegistration.getClientId()).isEqualTo("lssoClient");
    // ...
}

We can verify several properties here - such as the grant type or scopes.

Finally, let’s test the “/profile-simple” endpoint we previously created, using our “custom” Client as seen above:

@Test
public void givenRealClient_whenCallingEndpoint_thenSuccess() throws Exception {
    this.mvc.perform(get("/profile-simple").with(oauth2Client().clientRegistration(this.clientRegistrationRepository.findByRegistrationId("custom"))))
        .andExpect(status().isOk());
}

Notice that here we have provided the ClientRegistration using the ClientRegistrationRepository we autowired.

When we run the test we can see that is passes successfully.

3. Resources

- Spring Security Reference - OAuth 2.0 Client

- Next Generation OAuth Support with Spring Security 5.0

- Spring WebClient and OAuth2 Support

Lesson 1: OAuth2 and SPAs (theory) (text-only)

1. Goal

In this new lesson out of Learn Spring Security OAuth we’re going to discuss Single Page Applications, focusing on how to use a SPA client with OAuth 2.

2. Lesson Notes

We’ll focus only in understanding why SPAs have gained popularity over the last years and on the considerations we have to take into account with this approach, with no code involved at this point.

2.1. Brief Background on Web Applications Evolution

First, let’s take a step back and discuss the background of how web applications have evolved over the last few years and why SPAs have gained popularity in the process.

In the traditional, MVC-style web application, the server would generate the entire web page on the server-side and just return that to the browser.

For example, in a Spring MVC application, navigation would mean different requests resulting in different HTML documents being rendered by the browser.

But, as both REST APIs and Javascript have gained popularity, this approach changed and evolved as well.

2.2. Single Page Applications

As you can guess from the name, in Single Page Applications only one page is served and then navigation and all other user interactions are handled on the Client-side by manipulating the DOM directly, and making requests to the server APIs when needed.

Of course, the application heavily uses Javascript behind the scenes.

Naturally, there are a number of security and architectural aspects that should be taken into account when building an application such as this one. All these aspects are out of the scope of these SPA-related lessons.

A common solution is the Client application, which can be a Spring Boot application, simply exposing all the static resources like the actual HTML page, the CSS and the JavaScript files to be loaded by the browser.

After loading all the resources, the application running in the client-side can obtain an Access Token from the Authorization Server using the Authorization Code flow together with the PKCE extension, and then use it to access the secured resources in the Resource Server.

2.3. Considerations when Implementing SPAs

Now that we understand the big picture, we can touch on a few considerations we usually need to take into account when using a browser-based OAuth Client.

First, since the entire source is available to the browser, this Client cannot maintain the confidentiality of sensitive data, and therefore, it’s considered a public Client.

In this scenario, and in-line with the current guidance, we should be using the Authorization Code flow together with the PKCE extension.

When using the Authorization Code flow, then the Authorization Server may include a Refresh Token together with the Access Token response , although this can’t be guaranteed; this can differ based on the provider we use.

The reason is that this still represents a potential security vulnerability and, when provided, the guidance notes suggest the use of an expiration and rotation strategy to mitigate the attacks against the Refresh Token being leaked (for example, that a rotated refresh token lifetime shouldn’t extend the lifetime of the initial refresh token).

Finally, an important consideration that we’ll have to keep in mind is that the Client application will be communicating to the other services from the browser, so we may need to add CORS settings to the services if they belong to different origins.

3. Resources

Lesson 2: OAuth2 and SPAs (implementation) (text-only)

1. Goal

In this lesson, we’ll learn how to set up a simple Single Page Application (SPA) acting as an OAuth2 Client.

For this, we’ll use Javascript, therefore moving away from the traditional server-based MVC framework and the Spring Security Client support for a moment.

2. Lesson Notes

The relevant module you need to import when you’re working with this lesson is: lsso-module5/oauth2-and-SPAs

This lesson only needs a single reference codebase, so there is no end version of the project.

Note that all the changes we mention in the following sections are already included in the code. So, even though it’s useful to know exactly what we configured, you can simply start the services right away without having to change anything.

If you want to simply focus on the SPA Client implementation, then feel free to skip the next two subsections where we analyze the Authorization and Resource Servers setup.

2.1. Authorization Server Setup

Naturally, using a Single Page Application as a public OAuth2 Client has an impact on our registered Client configuration in the Authorization Server.

The first configuration we have to set up is making the Client public, which means it shouldn’t rely on Client Secrets as these can’t be properly secured. We can do this in Keycloak, by setting the Access Type option to public:

The next thing we have to update is the Redirect URI we’ll be using, as it should now point to a page in our new SPA client.

Also, use a plus (+) sign in the Web Origins input to allow all the Valid Redirect URIs origins for CORS (Cross-Origin Resource Sharing). This is necessary since now the Token Endpoint will be accessed from the browser and, in this case, from a different domain:

At this point, the Authorization Server would be able to work using PKCE if it detects a request with the corresponding parameters.

But it’s also a good practice to disable the possibility of using a plain code challenge method as this makes the protocol inefficient . We can do this by indicating only the S256 challenge method is allowed:

Finally, just to improve the functionality of the Client we also added some extra information to the existing user. Namely, the first name and a last name fields.

2.2. Enabling CORS on Our Resource Server

We need to prepare our Resource Server endpoints so that the CORS browser validations don’t block the communications.

First we have to indicate the security framework that we’ll be enabling CORS support and rely on a CORS configuration source bean:

public class ResourceSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors(withDefaults())
            .authorizeRequests()
            // ... 
    }
    // ...
}

and now we can define the corresponding bean:

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(singletonList("http://localhost:8082"));
    configuration.setAllowedMethods(asList("GET", "POST"));
    configuration.setAllowedHeaders(asList(HttpHeaders.AUTHORIZATION, HttpHeaders.CONTENT_TYPE));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

Here we’ve enabled the access to the Client host, particuarly for the GET and POST HTTP methods, and allowed the different headers that will be sent from our Client application.

And finally, we’ve configured the URL-based configuration source and registered it for all the endpoints.

2.3. Quick Look at the SPA Structure

Let’s analyze the SPA structure and then debug it to see how it executes this OAuth flow and how it interacts with our Resource Server to render the relevant information:

As we can see, our application is just serving static HTML, CSS and JS files; there is no endpoint declared or business logic executed on the server-side.

If we open the Client pom.xml in the dependencies section, we can verify that we’re not even using the OAuth Client dependencies in our project at all.

So, in practice, our entry point will be the index.html file, this will be the single page our application will be using and where all interactions will be happening.

In the index.html file we’ve declared the styles file, the libraries we’ll be using to get this working and the components that will take part in the execution of the application so that its functionality is accessible when loading this page.

Our main App component (src/main/resources/static/js/components/app.js) will contain the general state of the application and will be in charge of executing the main functionality of the page and of rendering the different components that come into play.

Let’s start the application and browse the client to see this in action.

2.4. SPA and Auth Code + PKCE in Action

A heads up before we get started is that we’re using a pop up modal to present the Authorization Endpoint whilst maintaining the state in our main page, so we need to enable this feature for our site.

Let’s browse http://localhost:8082/lsso-client to get presented the initial view of our SPA. Here we have some checkboxes we can use to indicate which scopes we want to request when asking for authorization in our Authorization Server.

Naturally, we don’t usually see this in real-case scenarios but here it will be handy to verify that the interactions with our Resource Server and our Authorization Server are getting executed correctly.

We’ll also open the Sources tab in the browser dev console to explore all the loaded static resources.

Open localhost:8082 -> lsso-client -> js -> components -> app.js and add a breakpoint in the onLoginFn definition so that the execution pauses before invoking the generateAuthURL function.

Step 1: Authorization Request

Let’s start by clicking on the LOGIN button and the execution should be paused at the breakpoint we just placed:

const onLoginFn = async () => {
  const { state, codeVerifier, authorizationURL, queryParamsObject } = await generateAuthURL();
  // ...
};

The first instruction is to generate the Authorization URL.

Let’s step in the generateAuthUrl:

const generateAuthURL = async () => {
  const { AUTH_URL, CLIENT_ID, CONFIGURED_REDIRECT_URI } = PROVIDER_CONFIGS;
  const state = generateState();
  console.log(`0- Generated state: ${state}`);
  const codeVerifier = generateCodeVerifier();
  console.log(`0- Generated Code Verifier: ${codeVerifier}`);
  const codeChallenge = await generateCodeChallenge(codeVerifier);
  console.log(`0- Generated Code Challenge from Code Verifier: ${codeChallenge}`);
  const scopesString = generateScopesString();
  const queryParamsObject = {
    client_id: CLIENT_ID,
    response_type: 'code',
    scope: scopesString,
    redirect_uri: CONFIGURED_REDIRECT_URI,
    state,
    code_challenge_method: 'S256',
    code_challenge: codeChallenge,
  };
  const params = new URLSearchParams();
  Object.entries(queryParamsObject).forEach(([key, value]) => params.append(key, value));
  const authorizationURL = `${AUTH_URL}\?${params.toString()}`;
  return { state, codeVerifier, authorizationURL, queryParamsObject };

As we step further we can see we’re creating the state and the codeVerifier as random values

The specs suggest a code_verifier with at least 256 bits of entropy. This can be achieved by generating a sequence of 32 random bytes.

Then we need to encode this octet sequence as base64url to produce a URL-safe string.

We are using the following base64url encoding function:

function base64urlencode(byteArray) {
  const stringCode = String.fromCharCode.apply(null, byteArray);
  const base64Encoded = btoa(stringCode);
  const base64urlEncoded = base64Encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  return base64urlEncoded;
}

We are creating a Code Verifier field with the following JavaScript function:

function generateCodeVerifier() {
  let randomByteArray = new Uint8Array(32);
  window.crypto.getRandomValues(randomByteArray);
  return base64urlencode(randomByteArray);
}

Back in the generateAuthUrl function, the Code Challenge is generated by transforming the Code Verifier. This is done using the SHA256 hash function, and then encoding the resulting octet sequence as base64url :slight_smile:

function generateCodeChallenge = async (codeVerifier) => {
  const strBuffer = new TextEncoder('utf-8').encode(codeVerifier);
  const hashBuffer = await window.crypto.subtle.digest('SHA-256', strBuffer);
  const hashedByteArray = Array.from(new Uint8Array(hashBuffer));
  return base64urlencode(hashedByteArray);
};

An important note is that this is a one-time-key; a unique Code Verifier (and its matching Code Challenge) should be created for every authorization request.

Of course, these are just reference implementations, we’re using features that might not be supported by all browsers, so relying on a library for this task is not a bad idea.

We then combine the active scopes in a string and finally we create the Authorization URL.

Note that we’re using only the Code Challenge and the Code Challenge method in the URL; the Code Verifier has to be sent afterwards when requesting the Access Token together with the Authorization Code so that the Authorization Server can match both codes.

We can continue the execution until we are back to the onLoginFn:

const onLoginFn = async () => {
  const { state, codeVerifier, authorizationURL, queryParamsObject } = await generateAuthURL();
  window.addEventListener('message', onChildResponseFn, { once: true, capture: false });
  console.log('1- Opening modal pointing to Authorization URL...');
  console.log('1- Query params:', queryParamsObject);
  let newModal = window.open(authorizationURL, 'external_login_page', 'width=800,height=600,left=200,top=100');
  setModal(newModal);
  setAuthRequest({
    codeVerifier,
    state,
  });
};

Now we have to send the request to the Authorization Endpoint, but we come across a limitation at this point i.e. we’ll need to use the Code Verifier we generated later.

So, if we use this same browser window to make the request, the internal state of the page would get lost including the code.

One solution would be to temporarily store the value, for example, using Cookies.

Another approach, which is the one we have followed here, is to create a child window, a modal to make the request and then communicate between these two windows to maintain the internal state here.

We have to take into account that with this approach the user will have to allow our site to open popup windows.

So, what we’ll need to do is to create an event listener to receive the messages sent by the child window. As we can see, the onChildResponseFn function will be handling these messages, so let’s place a new breakpoint there.

As we step over, the modal pointing to the Authorization URL we created before is opened.

Finally, this method will be storing the state and the codeVerifier values in the internal state of the page since we’ll need them later.

Step 2: Authenticating With Authorization Server

Now we can continue the process by logging in with credentials john@test.com/123. The execution should stop at the onChildResponseFn breakpoint:

const onChildResponseFn = (e) => {
  const receivedValues = { state: e.data.state, code: e.data.authCode };
  console.log('2- Received state and Authorization Code from the modal');
  console.log('2- Values:', receivedValues);
  if (receivedValues.state !== refAuthRequest.current.state) {
    window.alert('Retrieved state [' + receivedValues.state + "] didn't match stored one! Try again");
    return;
  }
  const { CLIENT_ID, CONFIGURED_REDIRECT_URI } = PROVIDER_CONFIGS;
  const tokenRequestBody = {
    grant_type: 'authorization_code',
    redirect_uri: CONFIGURED_REDIRECT_URI,
    code: receivedValues.code,
    code_verifier: refAuthRequest.current.codeVerifier,
    client_id: CLIENT_ID,
  };

  requestAccessToken(tokenRequestBody);
};

Let’s see what is happening here, first, the modal passed the state and the Authorization Code back to the Client application.

Of course, the first step should be to check that the state is the same as we sent.

Next, we create the tokenRequestBody that we’ll be sending to request the Access Token using the Authorization Code and the Code Verifier.

Step 3 & 4: Requesting and Receiving an Access Token

As we step into the requestAccessToken method:

const requestAccessToken = (tokenRequestBody) => {
  // ...
  Object.entries(tokenRequestBody).forEach(([key, value]) => params.append(key, value));
  console.log('3- Sending request to Token Endpoint...');
  console.log('3- Body:', tokenRequestBody);
  axios
    .post(PROVIDER_CONFIGS.TOKEN_URI, params, { headers })
    .then((response) => {
      const newAuth = response.data
      console.log('4- Received Access Token:', newAuth);
      setTimeoutForRefreshToken(newAuth);
      fetchUserInfo(newAuth);
      setAuth(newAuth);
    })
    // ...
};

Since Axios is Promise-based, we’ll add a breakpoint inside the lambda function of the then clause, which will get invoked eventually, if the request succeeds.

We resume the debugger to proceed with the retrieval of Access Token.

An important note is that since we’re actually using the Authorization Code flow, the Authorization Server might retrieve a Refresh Token giving us the possibility of setting up a periodic task to obtain a valid Access Token for longer.

In the setTimeoutForRefreshToken function we’re setting up a background task to periodically refresh the token.

Step 5: Fetching Secured Resources

Now with the Access Token we can fetch the User information or we can store it in the state of the page to use it in some other process later.

Let’s see how we use the authorization data to fetch User information:

const fetchUserInfo = (newAuth) => {
  // ...
  const headers = newAuth
      ? {
          headers: {
            Authorization: 'Bearer ' + newAuth.access_token,
          },
        }
      : {};
  console.log('5- Fetching User Info...');
  axios
    .get(USERINFO_URI, headers)
    .then((response) => {
      const username = extractProfileField(response.data, USER_FIELDS.USERNAME);
      const lastName = extractProfileField(response.data, USER_FIELDS.LAST_NAME);
      const firstName = extractProfileField(response.data, USER_FIELDS.FIRST_NAME);
      const picture = extractProfileField(response.data, USER_FIELDS.PICTURE);
      const user = { username, firstName, lastName, picture };
      setUser(user);
    })
    // ...
};

A similar process is followed in the Projects Page component to fetch the data from the Resource Server. As we resume the execution by closing the dev console, we can see the list of Projects is rendered in the page.

Since we requested the write scope, we can create new Projects and the view will be updated right away.

With this information stored in the internal memory of the application we can navigate through the different views and the application will be able to render it accordingly and refreshing the information when suitable.

However, if we refresh the whole page (F5) then the internal state is lost, and we’ll have to log in again to access the secured resources. We won’t be covering how to persist information between requests in this lesson. This, as with many other different aspects we have to consider when implementing a SPA, has to be properly analyzed as it can represent a security vulnerability for our application.

3. Resources