spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.87k stars 5.92k forks source link

Provide support for OAuth 2.0 Token Exchange for client #5199

Closed jgrandja closed 9 months ago

jgrandja commented 6 years ago

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

Related #6053

William1104 commented 6 years ago

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

jgrandja commented 6 years ago

@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?

mplukas commented 5 years ago

@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).

kdhindsa commented 5 years ago

@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?

jgrandja commented 5 years ago

@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.

kdhindsa commented 5 years ago

@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.

andifalk commented 4 years ago

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.

jgrandja commented 4 years ago

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

emedina commented 4 years ago

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

jgrandja commented 4 years ago

@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.

ZxShirley commented 4 years ago

@jgrandja Does this issue get resolved now?

jgrandja commented 4 years ago

@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.

muskiehunter1985 commented 3 years ago

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.

jgrandja commented 3 years ago

@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.

sclorng commented 3 years ago

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.

jgrandja commented 3 years ago

@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.

mmo21 commented 3 years ago

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

jgrandja commented 3 years ago

@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.

chenrujun commented 3 years ago

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

paulceli commented 2 years ago

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

ThomasKasene commented 2 years ago

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.

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 commented 1 year ago

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.

jgrandja commented 1 year ago

@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.

ThomasKasene commented 1 year ago

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.

jgrandja commented 1 year ago

@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.

ThomasKasene commented 1 year ago

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?
jgrandja commented 1 year ago

@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.

gsustek commented 1 year ago

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

ThomasKasene commented 1 year ago

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!)

uladzimir-shymanski commented 1 year ago

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

ciis0 commented 1 year ago

you promised to include it

tbf: that's not fair; I don't think the spring security team has promised it, from my understanding it was under considerations, but priorities shift...

Which provider do you intend to use?

I / the project I am in considered it for ADFS and Keycloak, but neither implements the RFC yet and have their own protocols (though keycloak's token exchange is inspired by the RFC, but as they say in the docs, it's a very loose implementation).

jgrandja commented 1 year ago

@ciis0 Thank you for coming to my defense :smile:

@uladzimir-shymanski There were no promises from my end. In fact, we as a team ensure that we don't make promises for features in a specific release. As @ciis0 mentioned, priorities are constantly changing so feature requests may get pushed out.

Furthermore, there is at most 2 resources on the Spring Security team that work on the OAuth support. I'm dedicated full time but I'm also leading Spring Authorization Server which takes most of my time. @sjohnr is the other resource which has been a huge help but he also works on other parts of Spring Security. So I hope you can appreciate we have limited resources devoted to the OAuth support within the Spring Security projects. And as I have mentioned here and here, no one from the community has submitted a PR for this feature, so it continues to wait in the backlog.

If you are eager for this feature, I have a task for you if you are up for it? Before we integrate this feature into the codebase, we need to do some preliminary research on which providers have actually implemented the server-side support for this RFC. Are you able to compile this list for us?

If there are many providers that have implemented the server-side of this RFC, this feature may be scheduled sooner. If there are only a couple of providers that have implemented, then it will continue to wait in the backlog.

victorherraiz commented 1 year ago

In our case, we use Ping Federate, with private_key_jwt for the client assertion. As far as I know there are several providers that support this grant.

https://docs.pingidentity.com/r/en-us/pingfederate-113/pf_config_oauth_token_exchange

At the moment, we are usign a custom library that add filter to the WebClient (acts before the actual call to get the access-token). It could be great to build on top of Spring Security for this functionality.

andifalk commented 1 year ago

@jgrandja The list of providers implementing token exchange (rfc8693) is growing. Here is a first quick list of some well-known providers:

As of now, none of the public cloud providers (Azure AD, AWS Cognito, Google Cloud Identity) offer any support for rfc8693 so far.

Milesy commented 1 year ago

@ThomasKasene @jgrandja - Is it currently possible to implement token exchange at all at present, even by using a custom provider etc as documented in the "How-to: Implement an Extension Authorization Grant Type" document?

I have been giving it a go, but running into confusion about how ClientRegistration fits into it, given all the provided builders used in other provider classes are making this assumption of an existing registration.

ciis0 commented 1 year ago

I think token exchange might not be a client registration but something you enable on the interceptor.

For Keycloak I implemented it like this: https://gist.github.com/ciis0/d0bcd5d0a51428a4192b598ee369b3b5

Likely something similar "simply" needs to be added to the built-in interceptors.

Milesy commented 1 year ago

@ciis0 - we have no concept of a client registration. In my use case my access token I want to exchange comes from some other IDP of which we have multiple, we want to exchange it for a common identity to use within the platform. This way all the microservices dont need to implement logic for 5 different types of identity provider.

So lets say forgerock issues the access token, it has a set of basic claims to the system. This is passed in at the API gateway, verified and exchanged for an internal identity token that can be passed to multiple back end services.

ciis0 commented 1 year ago

hmm, are you aware this issue is about implementing Token Exchange as described in RFC 8693?

I am having trouble applying the RFC to your use-case; the RFC from my understanding "just" describes an extension to the OAuth Token Endpoint to exchange tokens from one audience/scope for another audience/scope; but in my understanding "only" between a client and one Authorization Server / Secure Token Server; it's no abstraction for one a client and multiple AS/STS.

jgrandja commented 1 year ago

@Milesy Questions are better suited to Stack Overflow. We prefer to use GitHub issues only for bugs and enhancements. Please post your question there.

estambakio-sc commented 1 year ago

hmm, are you aware this issue is about implementing Token Exchange as described in RFC 8693?

I am having trouble applying the RFC to your use-case; the RFC from my understanding "just" describes an extension to the OAuth Token Endpoint to exchange tokens from one audience/scope for another audience/scope; but in my understanding "only" between a client and one Authorization Server / Secure Token Server; it's no abstraction for one a client and multiple AS/STS.

I think this is correct. Token exchange is an extension for OAuth server, not a client-side thing. So if it's implemented in Spring ecosystem then it should be implemented maybe in Spring Auth Server.

@ciis0 - we have no concept of a client registration. In my use case my access token I want to exchange comes from some other IDP of which we have multiple, we want to exchange it for a common identity to use within the platform. This way all the microservices dont need to implement logic for 5 different types of identity provider.

So lets say forgerock issues the access token, it has a set of basic claims to the system. This is passed in at the API gateway, verified and exchanged for an internal identity token that can be passed to multiple back end services.

For very similar use case I've used Keycloak as the "main" auth server, which is then configured to delegate to external IdP systems and allow token exchange etc. Spring cloud gateway which stands in front of the system has Spring security configured to point to the Keycloak, and passes its own JWT tokens downstream, so services have simple security config which also points to Keycloak and expects JWT. If services need to call external APIs then gateway has filters which request remote tokens using token exchange configured in Keycloak. If you follow similar pattern then to replace Keycloak with Spring auth server (if needed) I think token exchange should be implemented in this auth server (because it's a part of OAuth spec/flow), and auth clients (applications) shouldn't have this logic inside somehow.

ciis0 commented 1 year ago

Token exchange is an extension for OAuth server, not a client-side thing. So if it's implemented in Spring ecosystem then it should be implemented maybe in Spring Auth Server

While I agree the server side would be implemented in Spring Authorization Server, I'd say there still is a client part -- having one token, getting another one -- which would be implemented in Spring Security (like other iteraction with the token endpoint, like Client Credentials or Refresh Token grant.)

I think the challenge is where to implement the client part. My ad-hoc implementation suggest at least the interceptors need to be aware of what the target audience/scopes is/are and use common logic (the TokenExchangeClient in my code) to actually exchange the token.

My client relies on the Authorized Client classes, which suggest that the common logic could be integrated there, maybe OAuth2AuthorizeRequest could be extended with attributes to enable token exchange? I think primarily about target audience/scope attributes, not sure about secondary ones.

jgrandja commented 11 months ago

@andifalk Thank you for providing the list of supporting providers.

Based on that, we will provide support for RFC 8693 OAuth 2.0 Token Exchange in Spring Security (client-side) and Spring Authorization Server (server-side).

However, I'm not sure yet if we can provide this new support in Spring Security 6.3 and Spring Authorization Server 1.3.

There are a couple of higher priority items that we need to deal with first and depending on when those complete we can then decide if this feature will get into the next release.

All I can say now is that we will do our absolute best and keep you all updated as we progress over the current release cycle.

sjohnr commented 9 months ago

Initial support is added via 85c3d0ab13212b6df710207024b7240c71d2ebef. I've opened gh-14698 as a follow-up to add reference documentation, and hope to have a blog post available shortly as well.