spring-security icon indicating copy to clipboard operation
spring-security copied to clipboard

Provide support for OAuth 2.0 Token Exchange for client

Open jgrandja opened this issue 6 years ago • 20 comments

We need to provide support for OAuth 2.0 Token Exchange RFC 8693

Related #6053

jgrandja avatar Apr 03 '18 16:04 jgrandja

Hi,

This feature is removed from the 5.2.x milestone. May I know if any plan about this feature? Many thanks.

Thanks and regards, William

William1104 avatar Nov 08 '18 01:11 William1104

@William1104 We are planning on implementing this feature but it may be too early at the moment until the spec goes through the review process further.

Instead we replaced this feature with #6053. As an FYI, you can also exchange a JWT token for another JWT using the JWT Bearer grant.

I'm curious, are you aware of any providers that have implemented OAuth 2.0 Token Exchange?

jgrandja avatar Nov 08 '18 13:11 jgrandja

@jgrandja

I'm curious, are you aware of any providers that have implemented OAuth 2.0 Token Exchange?

One example would be Keycloak (https://www.keycloak.org/):

Token exchange in Keycloak is a very loose implementation of the OAuth Token Exchange specification at the IETF (https://www.keycloak.org/docs/6.0/securing_apps/#_token-exchange).

mplukas avatar Aug 14 '19 14:08 mplukas

@jgrandja I am facing this problem where spring security isn't sending the scopes to auth server (azure). I traced it back to OAuth2AuthorizationCodeGrantRequestEntityConverter.java which is ignoring the "scope" and "resource" parameters that are present in the request parameter in that class.

Is there a way I can override this behavior?

kdhindsa avatar Sep 16 '19 16:09 kdhindsa

@kdhindsa The issue you are having is not related to this issue (Token Exchange). Please post this question on StackOverflow or log a new issue if you believe this is a bug. Please see guidelines on using GitHub Issues.

spring security isn't sending the scopes to auth server (azure)

Have you configured the scopes property for the ClientRegistration?

Please see the reference doc for more details. I suspect there is a misconfiguration.

jgrandja avatar Sep 18 '19 19:09 jgrandja

@jgrandja, yes, I had configured the scopes correctly:

spring.security.oauth2.client.registration.azure.scope=openid,user.read,offline_access,files.read.all

but that didn't work. Eventually I found this configuration:

http.oauth2Login()
        .tokenEndpoint()
        .accessTokenResponseClient(aadAccessTokenResponseClient());

So I ended up creating my custom response client service which manually injects scopes:

public class AADOAuth2AuthorizationCodeGrantRequestEntityConverter
    extends OAuth2AuthorizationCodeGrantRequestEntityConverter {

  @Override
  public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
    RequestEntity requestEntity = super.convert(authorizationCodeGrantRequest);
    LinkedMultiValueMap<String, String> params = (LinkedMultiValueMap<String, String>)requestEntity.getBody();

    // FIXME: read from config
    params.put("scope", Arrays.asList("openid user.read offline_access files.read.all"));
    params.put("resource", Arrays.asList("https://graph.microsoft.com"));
    return requestEntity;
  }
}

and that worked.

kdhindsa avatar Sep 23 '19 02:09 kdhindsa

Hi @jgrandja, after lots of draft versions, the corresponding RFC 8693 standard for token exchange has finally been published this week (https://tools.ietf.org/html/rfc8693). So it would be great if you could schedule this in one of the next milestones.

andifalk avatar Jan 16 '20 20:01 andifalk

Thanks for the heads up @andifalk. I don't think we'll be able to get this into 5.3 (due Mar 4) as we have other priority tasks that need to be completed. We'll likely target 5.4

jgrandja avatar Jan 17 '20 18:01 jgrandja

This issue seems quite old now... Is this feature still in the roadmap for Spring Security?

https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16

emedina avatar Apr 03 '20 11:04 emedina

@emedina RFC 8693 was just published in Jan 2020, as mentioned in this comment. Now that it's published, we will see which providers implement to determine the appropriate time to implement on our end.

At the same time, features will get implemented quicker by the community via PR's as our team only has so much bandwidth. As of now, this feature is not scheduled for 5.4 but if a PR comes in then we will consider it then.

jgrandja avatar Apr 03 '20 17:04 jgrandja

@jgrandja Does this issue get resolved now?

ZxShirley avatar Sep 02 '20 10:09 ZxShirley

@ZxShirley It's not scheduled as of yet. As mentioned in my previous comment...

...features will get implemented quicker by the community via PR's as our team only has so much bandwidth. As of now, this feature is not scheduled for 5.4 but if a PR comes in then we will consider it then.

We'll be prioritizing features when we plan for 5.5, which will be towards end of this month.

jgrandja avatar Sep 02 '20 17:09 jgrandja

Any update on when this will be prioritized as RFC 8693 was been defined for over a year now? Many providers are supporting this now such as keycloak, ping federate, etc.

muskiehunter1985 avatar Mar 30 '21 12:03 muskiehunter1985

@muskiehunter1985 We won't be able to get this into 5.5.0.

However, I've scheduled it for 5.6.0, which should be released around Nov 2021.

We'll aim to get it into the 5.6.0-M1 release.

jgrandja avatar Mar 31 '21 11:03 jgrandja

Does this issue cover the urn:ietf:params:oauth:token-type:access_token subject token type ? If so, the current implementation of JwtBearerOAuth2AuthorizedClientProvider would be near the same. Only the Converter would map assertion to subject_token parameters. And subject_token_type and grant_type should be given accordingly.

Jwt Bearer may works well but, as per the RFC, the AS must validate the audience claim value is its token endpoint uri. This block all use cases where a microservice would request a new token on behalf of the current request token (audience is the microservice itself not the AS), which, IMHO is a far more common use case than requiring the microservice to forged/signed a Jwt Bearer for a given subject.

Unfortunaletly, some classes are not public and make difficult to write its own provider (OAuth2AuthorizationGrantRequestEntityUtils or AbstractOAuth2AuthorizationGrantRequestEntityConverter). As an example, https://github.com/jgrandja/oauth2-protocol-patterns/blob/main/microservice-b/src/main/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestEntityConverter.java won't compile anymore. This is not specific to this issue as OAuth2 is an extensible framework and it's not an unusual use case to implement its own grant type.

sclorng avatar May 25 '21 18:05 sclorng

@scrocquesel

Does this issue cover the urn:ietf:params:oauth:token-type:access_token

Yes. It's defined in Section 3. Token Type Identifiers

As an example, ...JwtBearerGrantRequestEntityConverter.java won't compile anymore

The oauth2-protocol-patterns sample has not been upgraded to Spring Security 5.5 as of yet and this class will be removed when updated which will resolve the compile error.

jgrandja avatar May 26 '21 08:05 jgrandja

Is this still scheduled it for 5.6.0, which should be released around Nov 2021?

mmo21 avatar Sep 14 '21 13:09 mmo21

@mmo21 Unfortunately, this won't make it into 5.6. We've been quite busy with Spring Authorization Server this year and there hasn't been anyone from the community to contribute to this.

jgrandja avatar Sep 15 '21 09:09 jgrandja

Hi, @kdhindsa, @jgrandja.

@jgrandja, yes, I had configured the scopes correctly:

spring.security.oauth2.client.registration.azure.scope=openid,user.read,offline_access,files.read.all

but that didn't work. Eventually I found this configuration:

http.oauth2Login()
        .tokenEndpoint()
        .accessTokenResponseClient(aadAccessTokenResponseClient());

So I ended up creating my custom response client service which manually injects scopes:

public class AADOAuth2AuthorizationCodeGrantRequestEntityConverter
    extends OAuth2AuthorizationCodeGrantRequestEntityConverter {

  @Override
  public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
    RequestEntity requestEntity = super.convert(authorizationCodeGrantRequest);
    LinkedMultiValueMap<String, String> params = (LinkedMultiValueMap<String, String>)requestEntity.getBody();

    // FIXME: read from config
    params.put("scope", Arrays.asList("openid user.read offline_access files.read.all"));
    params.put("resource", Arrays.asList("https://graph.microsoft.com"));
    return requestEntity;
  }
}

and that worked.

About this topic, I created an new issue: https://github.com/spring-projects/spring-security/issues/10452

chenrujun avatar Oct 29 '21 01:10 chenrujun

My humble attempt at a workaround : https://github.com/paulceli/spring-token-exchange-keycloak

paulceli avatar Sep 30 '22 13:09 paulceli

I'm looking at this and I'd like a little bit of guidance as to how to interpret the specification and how to follow Spring Security's conventions.

My uncertainties revolve around the two flavors of examples seen in the spec, under 2.3 Example Token Exchange, and under Appendix A. Additional Token Exchange Examples, respectively.

  • In the first example, the subject_token_type is urn:ietf:params:oauth:token-type:access_token and the access token from the authenticated user is being passed directly to the subject_token request parameter.
  • In the second set of examples, it seems as though the resource server has issued a JWT and is sending this as the subject_token, along with a subject_token_type equal to urn:ietf:params:oauth:token-type:jwt.
  • In the very last example, there's an actor_token and actor_token_type (delegation, rather than impersonation.)

My problem is figuring out how to choose which one of these methods to use for subject_token, and also, when to send the actor_token/actor_token_type request parameters, and when not to. I can only assume that all of this is up to the resource server, but how should Spring Security decide which strategy to use?

  1. Do we choose a default and then let the rest be configurable? For example, always get the subject_token from the Authorization header, and don't send any actor_token.
  2. Do we introduce subjectTokenType and actorTokenType in the ClientRegistration class, and based on their values, choose a strategy? For example, if ClientRegistration.subjectTokenType is urn:ietf:params:oauth:token-type:access_token, we pass on the token we got in the Authorization header, and if it's urn:ietf:params:oauth:token-type:jwt, we generate our own JWT.

Hopefully this hasn't been too much rambling from my part.

(PS: I can't guarantee that my efforts will actually amount to anything in the end, but either way, the questions I raise here will maybe help others working on this issue.)

ThomasKasene avatar Nov 27 '22 22:11 ThomasKasene

I'd like some feedback as to whether or not I'm going in the completely wrong direction here:

Is it okay to introduce new static methods in ServletOAuth2AuthorizedClientExchangeFilterFunction for easily adding audience and resource values to the request context, just like the pre-existing ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId(String) and ServletOAuth2AuthorizedClientExchangeFilterFunction.authentication(Authentication) methods? For example, something along the lines of this:

public static Consumer<Map<String, Object>> audience(String audience) {
    return (attributes) -> attributes.put(OAuth2AuthorizationContext.AUDIENCE_ATTR_NAME, audience);
}

And just to give an example of how it might look when setting up the WebClient call:

webClient
    .get()
    .uri("/rest/v1/lastname")
    .accept(MediaType.APPLICATION_JSON)
    .attributes(clientRegistrationId("lastname-api-client"))
    .attributes(audience("lastname-api"))                      // <- new
    .attributes(resource(URI.create("http://localhost:8083"))) // <- new
    // ...

If not, what's the preferred way of passing additional information into the token exchange request? Reading between the lines, I've come to the conclusion that adding anything at all to ClientRegistration is probably not desirable, so this was the best alternative I could find so far.

ThomasKasene avatar Dec 06 '22 22:12 ThomasKasene

@ThomasKasene I haven't had a chance to re-review the spec lately. I appreciate that users are looking for this feature but we've been very busy with many other priorities and we only have so much bandwidth. No one has offered to take this feature on so this slows things down as well.

Is it okay to introduce new static methods in ServletOAuth2AuthorizedClientExchangeFilterFunction for easily adding audience and resource values to the request context

No. This would not be the approach we would use. We would most definitely introduce a new OAuth2AccessTokenResponseClient and OAuth2AuthorizedClientProvider that ultimately would be composed in OAuth2AuthorizedClientManager.

I've come to the conclusion that adding anything at all to ClientRegistration is probably not desirable

Correct.

jgrandja avatar Dec 14 '22 12:12 jgrandja

I'm slowly chipping away at the spec/implementation as I find the time, and I recently discarded the idea of manipulating the ExchangeFilterFunction. I have added such methods to my OAuth2AuthorizedClientProvider as there seems to be a definite need for them, however. But we'll see how it all turns out in the end. Anyway, I appreciate you taking the time to let me know!

Currently, my biggest obstacle is figuring out how to make it create and sign the actor token. I couldn't find any easily re-usable code which does this, so I was forced to draw inspiration (mostly copy) from NimbusJwtClientAuthenticationParametersConverter.

ThomasKasene avatar Dec 14 '22 15:12 ThomasKasene

@ThomasKasene Please reach out to me in January as we should have our 6.1 plan in place. If this is scheduled for 6.1 then contributions would be very helpful.

jgrandja avatar Dec 14 '22 17:12 jgrandja

I'll do that. In the meantime, I've got a couple more questions to ponder:

  1. Considering that the acting party can appear in several token exchanges (for example, an admin or a support user), and the fact that a principal may choose to delegate access to multiple such actors, is it even possible to manage all those OAuth2AuthorizedClients with the current APIs (OAuth2AuthorizedClientRepository, etc)?
  2. Adding to that, what do we do if a client chooses to obtain an authorized client both with the client_credentials grant, and with the token-exchange grant, where it uses itself as the actor? Those principals would be identical (I think), and with matching clientRegistrationIds they would be difficult for the OAuth2AuthorizedClientRepository implementations to distinguish.
  3. Is it acceptable to add a new constant, org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType.N_A, to represent the value registered as part of RFC8693?

ThomasKasene avatar Dec 20 '22 18:12 ThomasKasene

@ThomasKasene I won't be able to answer your questions until I review the spec in detail, which won't happen until after the new year.

jgrandja avatar Dec 20 '22 19:12 jgrandja

Any news on this topic? in which version, we could expect this?

gsustek avatar May 09 '23 10:05 gsustek

There's no news from me, life happened and I've been somewhat absent these last few months.

The specification is a very flexible one, and it's difficult to shoehorn the entire thing into Spring Security without making some changes to several of the existing APIs, at least from my limited understanding of Spring Security. The main complexity comes from the fact that a user should be able to authorize up to several other users (or applications) to act on their behalf, so the model becomes something like this:

principal + client registration ID + actor1 -> OAuth2AuthorizedClient (1) principal + client registration ID + actor2 -> OAuth2AuthorizedClient (2) principal + client registration ID + actor3 -> OAuth2AuthorizedClient (3)

Whereas the APIs within Spring Security only allows for this model:

principal + client registration ID -> OAuth2AuthorizedClient

(I'm hoping somebody smarter than me will correct me if I've misunderstood these APIs!)

ThomasKasene avatar May 14 '23 18:05 ThomasKasene

@jgrandja, Any news regarding adding it into the upcoming milestones? Almost 3 years left since the time you promised to include it..

uladzimir-shymanski avatar Nov 14 '23 21:11 uladzimir-shymanski