Skip to content

Wrong JWT token sub attribute after second authentication #384

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jacksoncastro opened this issue Aug 3, 2021 · 5 comments
Closed

Wrong JWT token sub attribute after second authentication #384

jacksoncastro opened this issue Aug 3, 2021 · 5 comments
Assignees
Labels
status: invalid An issue that we don't feel is valid

Comments

@jacksoncastro
Copy link

jacksoncastro commented Aug 3, 2021

Describe the bug

I have an environment composed of a SPA, a resource server and an authorization server using Spring's new library.
SPA authorizes using the Authorization Code + PKCE flow.

Lib versions:

spring-boot-starter-parent: 2.5.2
spring-security-oauth2-authorization-server: 0.1.0

I'm having a somewhat complex problem to explain, but come on.

During the first authorization of the SPA application, which in turn is redirected to the login screen of the authorization server, I get an access_token with the JWT's "sub" attribute with the desired value (any user identifier), as a response. for example "bob".

When the token expires, a new authorization request is started, which in turn ends up returning a new access_token with the wrong "sub" attribute. Its value turns out to be the customer id and not the user identifier.

I'm going to divide this troubleshooting into 2 steps: 1st and 2nd authentication.

1st authentication (not yet logged into the Authorization Server)

During the first authentication that occurs on the endpoint /oauth2/authorize, the Principal is validated, which was passed on by the login form and created an authentication of the UsernamePasswordAuthenticationToken type, which in turn implements the org.springframework.security.core.Authentication interface.

This object is saved within the user's session in the SPRING_SECURITY_CONTEXT attribute.

After that, the code is created and saved in authorizationService.save(). For the Authentication#principalName field, the authentication username (username entered in the login screen) is used.

In the request back to /oauth2/token it is seen if there is a session and the Principal is searched via the SPRING_SECURITY_CONTEXT attribute. The correct Principal is loaded. The JWT "sub" is issued with the correct value which is the username.

Following the flow, the org.springframework.security.oauth2.server.authorization.web.DelegatingAuthenticationConverter class registers 3 converters:

OAuth2TokenEndpointFilterCustom
 -> converters.add(new AuthorizationCodeAuthenticationConverter());
    converters.add(new RefreshTokenAuthenticationConverter());
    converters.add(new ClientCredentialsAuthenticationConverter());

And ends up using the ClientCredentialsAuthenticationConverter converter to create the authorization on top of the request. Here authorization is wrong with application name (spa-client).

The first token ends up being issued correctly because its match is made by code, as shown in the following line:

OAuth2Authorization authorization = this.authorizationService.findByToken(
				authorizationCodeAuthentication.getCode(), AUTHORIZATION_CODE_TOKEN_TYPE);

So the wrong authorization is saved in the session.

2nd authentication

At the beginning of authentication it is seen that already exists session. Here the wrong authentication is retrieved and passed to the JWT "sub".

The framework uses this wrong value until it returns the token.

Comments:

  • The provider responsible for the first authorization is org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
  • The provider responsible for the second is org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider because the user is already logged into the authorization server;
  • The sub is mounted on top of the Principal's name attribute.

Expected behavior

In all authentications, even if the user is logged into the authorization server, the JWT's "sub" attribute must come from the username.

Examples of requests:

http://localhost:9000/oauth2/authorize?client_id=spa-client&redirect_uri=http%3A%2F%2Flocalhost%3A4200&response_type=code&scope=openid&nonce=XXXXX&state=XXXXX&code_challenge=XXXXX&code_challenge_method=S256
curl 'http://localhost:9000/oauth2/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Cookie: JSESSIONID=XXXXX' \
  --data-raw 'grant_type=authorization_code&client_id=sga-portal&code_verifier=XXXXX&code=XXXXX&redirect_uri=http%3A%2F%2Flocalhost%3A4200'

Examples of returns:

Correct:

{
  "sub": "bob", // username, it's OK!
  "aud": "spa-client",
  "nbf": 1627914460,
  "scope": [
    "read"
  ],
  "iss": "http://localhost:9000",
  "exp": 1627914490,
  "iat": 1627914460,
  "jti": "9a428545-d976-4640-81b7-2bfc9df42bb3"
}

Wrong:

{
  "sub": "spa-client", // SPA name is WRONG!
  "aud": "spa-client",
  "nbf": 1627914460,
  "scope": [
    "read"
  ],
  "iss": "http://localhost:9000",
  "exp": 1627914490,
  "iat": 1627914460,
  "jti": "9a428545-d976-4640-81b7-2bfc9df42bb3"
}
@jacksoncastro jacksoncastro added the type: bug A general bug label Aug 3, 2021
@jgrandja
Copy link
Collaborator

jgrandja commented Aug 9, 2021

@jacksoncastro The current behaviour is correct. When the client performs the client_credentials grant flow, it is acting on behalf of itself NOT on behalf of the user (authorization_code grant flow). Therefore, the sub in the token when the client_credentials grant flow is performed will be the client_id.

Take a look at the refresh_token grant flow, as this might be what you need to configure to refresh the expired access token and preserve the existing sub (user).

You might want to review the Client Credentials grant for further details.

@jgrandja jgrandja closed this as completed Aug 9, 2021
@jgrandja jgrandja self-assigned this Aug 9, 2021
@jgrandja jgrandja added status: invalid An issue that we don't feel is valid and removed type: bug A general bug labels Aug 9, 2021
@jacksoncastro
Copy link
Author

@jacksoncastro The current behaviour is correct. When the client performs the client_credentials grant flow, it is acting on behalf of itself NOT on behalf of the user (authorization_code grant flow). Therefore, the sub in the token when the client_credentials grant flow is performed will be the client_id.

Take a look at the refresh_token grant flow, as this might be what you need to configure to refresh the expired access token and preserve the existing sub (user).

You might want to review the Client Credentials grant for further details.

After upgrading to version 0.1.2 the error no longer exists, even if it is a client request (SPA).

Detail any new request to /oauth2/authorize (authorization_code) returns the sub correctly with this new version.

So in that case who is with the correct behavior?

@nickmelis
Copy link

@jgrandja we just stumbled across the same issue. Due to not being able to get refresh tokens using authorization_code flow with PKCE (as highlighted here and here), we're having to go back to /oauth2/authorize whenever our access_token expires.
Now, you're suggesting to

Take a look at the refresh_token grant flow, as this might be what you need to configure to refresh the expired access token and preserve the existing sub (user).

but we can't get a refresh token in the first place, so we're kind of stuck here. Any idea how to get around this? Thanks

@jgrandja
Copy link
Collaborator

jgrandja commented Feb 7, 2022

@nickmelis

Any idea how to get around this?

If the access token expires and there is no refresh token available, then the client needs to go through the authorization_code flow again.

@sjohnr
Copy link
Member

sjohnr commented Feb 7, 2022

@nickmelis take a look at what the OIDC spec says regarding prompt=none.

I recently set up a SPA to use authorization_code flow with PKCE. I was able to use the client's support for "silent renew" (or "silent authentication") to utilize the session to refresh the access token automatically. If your client does not support that, you may want to look into how client implementations support this feature. In my testing, it works out of the box without any user interaction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

4 participants