The spring-OAuth2 implementation of Spring based on OAuth2 protocol is an industry-level interface resource security solution. Based on this dependency, we can configure different permissions for different clients to access interface data.

Recommended reading

  • Springboot2.x tutorial summary

The default token generation method

Every time we obtain an access_token, the first token is returned by default. When the same user obtains the token multiple times, only the expiration time is shortened, and everything else remains the same.

This method has both advantages and disadvantages. If only one person can log in to the same account, there is no problem, but if more than one person can log in to the same account at the same time, there will be some problems.

Such as we now have an account called hengboy: the first person to login seasonal card is valid for we configure the longest validity (assuming for 7200 seconds), then have the second man the same user login again, the second personal access token validity will not be reset (May 3000 seconds left), for this kind of result is not we expected.

Cause analysis,

There are three ways to store tokens in spring-OAuth2 dependencies: InMemoryTokenStore, RedisTokenStore, and JdbcTokenStore.

From reading the source code, we can see that no matter what method we configure to store tokens, there will only be one valid token for the same account. Considering the above scenario, the token obtained by the second person is the same as that obtained by the first person.

DefaultTokenServices

DefaultTokenServices token service AuthorizationServerTokenServices is the default implementation of the interface, is located in the org. Springframework. Security. Oauth2. The provider. The token bags, Provides default methods for manipulating tokens. Common ones are:

  • createAccessToken: Creates request tokens based on client information, logged-in user information (access_token) and refreshing the token (refresh_token)
  • refreshAccessToken: Based on the refresh token (refresh_token) to get a new request token (access_token)
  • revokeToken: Revokes the token, deletes the user-generated request token (access_token), refresh token (refresh_token)

Source code analysis: generate tokens

DefaultTokenServices# createAccessToken:

@Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        if(existingAccessToken ! =null) {
            if(! existingAccessToken.isExpired()) {this.tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }

            if(existingAccessToken.getRefreshToken() ! =null) {
                refreshToken = existingAccessToken.getRefreshToken();
                this.tokenStore.removeRefreshToken(refreshToken);
            }

            this.tokenStore.removeAccessToken(existingAccessToken);
        }

        if (refreshToken == null) {
            refreshToken = this.createRefreshToken(authentication);
        } else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = this.createRefreshToken(authentication);
            }
        }

        OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
        this.tokenStore.storeAccessToken(accessToken, authentication);
        refreshToken = accessToken.getRefreshToken();
        if(refreshToken ! =null) {
            this.tokenStore.storeRefreshToken(refreshToken, authentication);
        }

        return accessToken;
    }
Copy the code

In the source method of creating the token, first read the token of the account in the storage medium (TokenStore implementation class) according to the authentication information, if the token has been stored and not expired, then return directly (this is also the logic of returning the same token when different people log on the same account), if the token has expired, The refresh_token and access_token are deleted and generated again.

Source code analysis: refresh the token

DefaultTokenServices# refreshAccessToken:

@Transactional( noRollbackFor = {InvalidTokenException.class, InvalidGrantException.class} )
    public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
        if (!this.supportRefreshToken) {
            throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
        } else {
            OAuth2RefreshToken refreshToken = this.tokenStore.readRefreshToken(refreshTokenValue);
            if (refreshToken == null) {
                throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
            } else {
                OAuth2Authentication authentication = this.tokenStore.readAuthenticationForRefreshToken(refreshToken);
                if (this.authenticationManager ! =null && !authentication.isClientOnly()) {
                    Authentication userAuthentication = authentication.getUserAuthentication();
                    PreAuthenticatedAuthenticationToken preAuthenticatedToken = new PreAuthenticatedAuthenticationToken(userAuthentication, "", authentication.getAuthorities());
                    if(userAuthentication.getDetails() ! =null) {
                        preAuthenticatedToken.setDetails(userAuthentication.getDetails());
                    }

                    Authentication user = this.authenticationManager.authenticate(preAuthenticatedToken);
                    Object details = authentication.getDetails();
                    authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
                    authentication.setDetails(details);
                }

                String clientId = authentication.getOAuth2Request().getClientId();
                if(clientId ! =null && clientId.equals(tokenRequest.getClientId())) {
                    this.tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
                    if (this.isExpired(refreshToken)) {
                        this.tokenStore.removeRefreshToken(refreshToken);
                        throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
                    } else {
                        authentication = this.createRefreshedAuthentication(authentication, tokenRequest);
                        if (!this.reuseRefreshToken) {
                            this.tokenStore.removeRefreshToken(refreshToken);
                            refreshToken = this.createRefreshToken(authentication);
                        }

                        OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
                        this.tokenStore.storeAccessToken(accessToken, authentication);
                        if (!this.reuseRefreshToken) {
                            this.tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
                        }

                        returnaccessToken; }}else {
                    throw new InvalidGrantException("Wrong client for this refresh token: "+ refreshTokenValue); }}}}Copy the code

In the source method of refreshing the token, we first need to read the refresh_token and throw an InvalidGrantException if the refresh_token does not exist.

According to refresh before the execution token refresh token request token removeAccessTokenUsingRefreshToken delete, delete to determine whether the refresh token again after failure, failure is thrown if InvalidTokenException anomalies.

Reuse of the refresh token is determined by the global variable reuseRefreshToken, which by default is true, meaning that the refresh token can be reused, However, the refresh token is recreated and replaced after createAccessToken > TokenEnhancer#enhance (this appears to be a Bug).

Rewrite TokenServices

Expect effect

Assume that the request token (access_token) is valid for 7200 seconds, or 2 hours, and the refresh token (refresh_token) is valid for 43,200 seconds, or 12 hours.

After the first token is obtained through createAccessToken, each request for a token (access_token) expires with a refresh (/oauth/token? Grant_type =refresh_token) obtains a new request token (valid for 2 hours). When refresh_token is invalid, the request token is obtained using createAccessToken.

Analyzing the desired effect

For the expected effect above, we need to modify the source code of the createAccessToken and refreshAccessToken methods. When calling the createAccessToken method, we will not determine whether to use the existing valid token. Calling the refreshAccessToken method removes the return field of the refresh_token response and binds the new request token to the refresh token.

OverrideTokenServices

Copy all the code in the DefaultTokenServices class and create a class named OverrideTokenServices. For compatibility with the original logic, add a global variable, alwaysCreateToken, that determines whether tokens are always created.

Override the token creation logic

@Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        OAuth2RefreshToken refreshToken = null;
        OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
        // Determine whether the token is always created based on the alwaysCreateToken field
        if (!this.alwaysCreateToken && existingAccessToken ! =null) {
            if(! existingAccessToken.isExpired()) {this.tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }

            if(existingAccessToken.getRefreshToken() ! =null) {
                refreshToken = existingAccessToken.getRefreshToken();
                this.tokenStore.removeRefreshToken(refreshToken);
            }

            this.tokenStore.removeAccessToken(existingAccessToken);
        }
        if (refreshToken == null) {
            refreshToken = this.createRefreshToken(authentication);
        } else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = this.createRefreshToken(authentication);
            }
        }
        OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
        this.tokenStore.storeAccessToken(accessToken, authentication);
        refreshToken = accessToken.getRefreshToken();
        if(refreshToken ! =null) {
            this.tokenStore.storeRefreshToken(refreshToken, authentication);
        }

        return accessToken;
    }
Copy the code

If we want to use the original logic, we need to set the value of the alwaysCreateToken variable to false when initializing the OverrideTokenServices class.

Rewrite the refresh token logic

public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
        if (!this.supportRefreshToken) {
            throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
        } else {
            OAuth2RefreshToken refreshToken = this.tokenStore.readRefreshToken(refreshTokenValue);
            if (refreshToken == null) {
                throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
            } else {
                OAuth2Authentication authentication = this.tokenStore.readAuthenticationForRefreshToken(refreshToken);
                if (this.authenticationManager ! =null && !authentication.isClientOnly()) {
                    Authentication userAuthentication = authentication.getUserAuthentication();
                    PreAuthenticatedAuthenticationToken preAuthenticatedToken = new PreAuthenticatedAuthenticationToken(userAuthentication, "", authentication.getAuthorities());
                    if(userAuthentication.getDetails() ! =null) {
                        preAuthenticatedToken.setDetails(userAuthentication.getDetails());
                    }

                    Authentication user = this.authenticationManager.authenticate(preAuthenticatedToken);
                    Object details = authentication.getDetails();
                    authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
                    authentication.setDetails(details);
                }

                String clientId = authentication.getOAuth2Request().getClientId();
                if(clientId ! =null && clientId.equals(tokenRequest.getClientId())) {
                    this.tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
                    if (this.isExpired(refreshToken)) {
                        this.tokenStore.removeRefreshToken(refreshToken);
                        throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
                    } else {
                        authentication = this.createRefreshedAuthentication(authentication, tokenRequest);
                        if (!this.reuseRefreshToken) {
                            this.tokenStore.removeRefreshToken(refreshToken);
                            refreshToken = this.createRefreshToken(authentication);
                        }

                        DefaultOAuth2AccessToken accessToken = (DefaultOAuth2AccessToken) this.createAccessToken(authentication, refreshToken);
                        // If you reuse the refresh token, set the refresh token to the new AccessToken
                        // If the refresh token is reused, the refresh token is bound to the newly generated request token
                        if (this.reuseRefreshToken) {
                            accessToken.setRefreshToken(refreshToken);
                        }
                        this.tokenStore.storeAccessToken(accessToken, authentication);
                        if (!this.reuseRefreshToken) {
                            this.tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
                        }
                        // No new token will be returned after refresh
                        // Refresh_token is no longer returned after refreshing the token
                        accessToken.setRefreshToken(null);
                        returnaccessToken; }}else {
                    throw new InvalidGrantException("Wrong client for this refresh token: "+ refreshTokenValue); }}}}Copy the code

The DefaultTokenServices class defines the global variable reuseRefreshToken by default. The value of this variable is true, indicating that refresh_token can be reused by default. Generally, refresh_token has a long expiration time. When the access_token becomes invalid, a new valid request token is obtained according to the refreshing token.

Configuration TokenServices

We need the AuthorizationServerConfigurerAdapter configuration of TokenServices replace used within the implementation class as shown below:

/** * instantiate {@link OverrideTokenServices}
  *
  * @return {@link OverrideTokenServices}
  */
private AuthorizationServerTokenServices tokenServices(a) {
  OverrideTokenServices tokenServices = new OverrideTokenServices();
  tokenServices.setTokenStore(tokenStore());
  tokenServices.setAlwaysCreateToken(true);
  tokenServices.setSupportRefreshToken(true);
  tokenServices.setClientDetailsService(clientDetailsService);
  return tokenServices;
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  endpoints
    .authenticationManager(authenticationManager)
    .tokenStore(tokenStore())
    // Configure the replacement to use TokenServices
    .tokenServices(tokenServices());
}
Copy the code

test

Example of getting a token:

Obtain the first token: yuqiyu@hengyu ~> curl -x post-u"local:123456" http://localhost:9091/oauth/token -d "grant_type=password&username=hengboy&password=123456"| jsonpp % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 199 0 147, 100, 52, 362-128: - : - : -- : -- -- - : - 491 {"access_token": "qoL7Kg33-deYw-aw8PnIKK-qxEk"."token_type": "bearer"."refresh_token": "-OfFqllKZJC6-r_v_uR9KGUBXl0"."expires_in": 7199,
  "scope": "read"} get the token for the second time: yuqiyu@hengyu ~> curl -x post-u"local:123456" http://localhost:9091/oauth/token -d "grant_type=password&username=hengboy&password=123456"| jsonpp % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 199 0 147, 100, 52, 896-317: - : - : -- : -- -- - : - 1213 {"access_token": "hfo01xMTVE1xxxbzQLY7vPfLXPE"."token_type": "bearer"."refresh_token": "QuLgm-H3xHzo71M_XSLrglsRs_o"."expires_in": 7199,
  "scope": "read"
}
Copy the code

It can be seen that the same account was used to obtain two tokens above, and the contents of these two tokens are completely different, which is to realize the requirement of returning a new token when different people log in with the same account.

Examples of refreshing tokens:

Refresh based on the refresh token obtained for the first time: yuqiyu@hengyu ~> curl -x post-u"local:123456" http://localhost:9091/oauth/token -d "grant_type=refresh_token&refresh_token=-OfFqllKZJC6-r_v_uR9KGUBXl0"| jsonpp % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 167 0 101 100 66 1109 725 - : -- : -- -- - : -- -- - : - 1835 {"access_token": "KuOprmzBCzC78NXlTkHvZGs9rhs"."token_type": "bearer"."expires_in": 7199,
  "scope": "read"} refresh according to the refresh token obtained the second time: yuqiyu@hengyu ~> curl -x post-u"local:123456" http://localhost:9091/oauth/token -d "grant_type=refresh_token&refresh_token=QuLgm-H3xHzo71M_XSLrglsRs_o"| jsonpp % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 167 0 101 100 66 1122 733 - : -- : -- -- - : -- -- - : - 1855 {"access_token": "aLPOEkfUCxn87XkTkcwyixaUO1s"."token_type": "bearer"."expires_in": 7200,
  "scope": "read"
}
Copy the code

The same account is refreshed twice, but the validity period of the token does not affect each other. The first refresh uses the refresh token obtained at the first time, which is actually the first request token refreshed, and has nothing to do with the second one!!

Code sample

If you like this article please click Star for source repository, thanks!! The sample source code for this article can be obtained in the oauth2-always-create-token directory:

  • Gitee:gitee.com/hengboy/spr…