spring-attic / spring-security-oauth

Support for adding OAuth1(a) and OAuth2 features (consumer and provider) for Spring web applications.
http://github.com/spring-projects/spring-security-oauth
Apache License 2.0
4.7k stars 4.04k forks source link

[SECOAUTH-425] Pre-established redirect URI should be intercepted to retrieve the access token and redirect to original URI #137

Open dsyer opened 10 years ago

dsyer commented 10 years ago

Priority: Major Original Assignee: Dave Syer Reporter: Nick Williams Created At: Sat, 5 Oct 2013 08:06:47 +0100 Last Updated on Jira: Tue, 8 Oct 2013 06:36:23 +0100

After reading SECOAUTH-96 and http://forum.spring.io/forum/spring-projects/security/oauth/118830-pre-established-redirect-uri-in-oauth-resource about 10 times and scouring the code, I'm convinced there's a huge disconnect here, and that this feature is NOT complete.

Here's a very simple use case that should be easily satisfied:

  1. Client registers with provider. Provider requires exactly one pre-registered client redirect URI for returning authorization codes. Provider does not support additional or non-registered redirect URIs. This is spec-compliant.
  2. User logs in to client.
  3. User accesses client feature that accesses protected resource on resource server.
  4. Client redirects user to authorization server.
  5. User logs in to authorization server.
  6. User grants authorization to the client on the authorization server.
  7. Authorization server redirects user to the client's pre-registered redirect URL.
  8. Client contacts authorization server to exchange authorization code for access token.
  9. Client redirects user to the original client URL that the user was trying to access.

This seems pretty simple and standard, but is not possible with Spring Security OAuth 1.0.5. SECOAUTH-96 described this use case, and so by marking it complete I would think that means the use case is supported. But it is not.

The first thing I did was get it working with the "current" URL instead of a pre-registered URL, to confirm nothing else was getting in the way. If I accessed /clientApp/foo and got redirected to the authorization server, the authorization server ultimately send me back to /clientApp/foo with the authorization code and state parameters. This worked great, and I was able to access protected resources after authorizing. (One big problem, though, is that the user is never redirected again after being returned to /clientApp/foo, so they land on a page where they can see the code and state parameters in their browser URL. That's a no-no.)

So then I set use-current-uri to false and pre-established-redirect-uri to http://myserver.com/clientApp/oauth/confirm and tried things again. The user is correctly redirected to the authorization server with http://myserver.com/clientApp/oauth/confirm as the redirect URL parameter. After authorizing, the authorization server correctly redirects the user to http://myserver.com/clientApp/oauth/confirm with the code and state parameters. However, at this point the client application returns a 404. I assumed there was some filter somewhere that intercepted the request because it works for "current" URLs, but after scouring the source code I can't actually find any code that intercepts requests to retrieve these parameters and exchange them for the access token. It's like magic, except that it doesn't work for a static, pre-registered URL.

Thinking that I needed a controller to at least back the request, I added a controller to match the http://myserver.com/clientApp/oauth/confirm URL. Now I don't get a 404 anymore, but the parameters are still never detected and exchanged for an access token. If I attempt to go back to the client URL that needed an access token to begin with, I simply get sent back to the authorization server to start all over again.

In essence, once I start using pre-established-redirect-uri, Spring Security OAuth 2 (for the client-side anyway) just stops working completely. This makes it essentially useless for clients with multiple URLs that access protected resources on a provider that permits only one, pre-registered redirect URI.

Maybe (hopefully) I'm missing something. But I spent a day reading documentation and looking at sample applications (and there are no samples that use pre-established redirect URIs), a week trying to get this to work, and a further two days just reading the source code. I can't get it to work and I don't know how it could possibly work based on what I've seen.

Comments:

david_syer on Sat, 5 Oct 2013 12:30:03 +0100

I'm pretty sure this works as designed, but I also see some room for improvement (along the lines of one of the ideas discussed in SECOAUTH-96, which actually was about something else). The sequence of events you list can and should be implemented by a client who wants to get a callback to the same URL every time, but parts of it aren't implemented by the framework yet (hence this issue has been marked as an improvement). In particular, the framework intercepts the redirect with the auth code in it (ClientContextFilter) and obtains an access token, but the handling of the request after that (i.e. a controller to prevent the 404 that you saw) is up to the client.

An automatic additional redirect is a possible implementation of the flow you describe, and that bit was discussed briefly as part of SECOAUTH-96. It's a bit like a remembered request flow after a form login in Spring Security, so I wonder if there are some reusable strategies there. But really I'm not sure it adds much value. Do you have a use case where you for some reason actually require the redirect to come to the same place every time?

guitarking117 on Sun, 6 Oct 2013 15:46:10 +0100

In particular, the framework intercepts the redirect with the auth code in it (ClientContextFilter) and obtains an access token

No, it doesn't, which is why I still think this is a bug. It does not intercept the redirect to /clientApp/oauth/confirm with the auth code in it. Once I get the 404, I can manually go back to the URL /clientApp/doSomething that tries to access the protected resource, and I just get redirected back to the authorization server again. The access token is only retrieved if the authorization server redirects to /clientApp/doSomething.

Also, I'm not sure why you say OAuth2ClientContextFilter intercepts the request and retrieves the access token; there is zero code in OAuth2ClientContextFilter that intercepts requests with authorization codes in them. This class only handles redirecting to the authorization server when the application throws a UserRedirectRequiredException. In fact, I have gone through the code of every class in Spring Security OAuth 2 that implements Filter (that I can find), and none of them retrieve the access token. The code that retrieves an access token is in the OAuth2RestTemplate, which IMO seems like not the right place for it. However, as of right now I still have not found the code that actually retrieves these state and authorization code parameters from the request redirected from the authorization server.

guitarking117 on Sun, 6 Oct 2013 15:54:16 +0100

I just found it. The RestTemplateBeanDefinitionParser creates a request-scoped DefaultAccessTokenRequest, which is given to the session-scoped DefaultOAuth2ClientContext, which is given to the OAuth2RestTemplate bean. When it creates the DefaultAccessTokenRequest it gives it the current request's request parameters. However, these request parameters are only accessed if the OAuth2RestTemplate is used during the course of the request. This will only happen if the user is redirected to /clientApp/doSomething. It won't happen if the user is redirected to /clientApp/oauth/confirm. Thus, it's broken.

guitarking117 on Sun, 6 Oct 2013 16:08:52 +0100

I briefly looked into how to fix this, but it's going to take more work than I have time right now to put together a pull request. Hopefully someone more familiar with the project can figure it out more quickly.

What I did discover is that the interception logic cannot be put in OAuth2ClientContextFilter. There is only one OAuth2ClientContextFilter, but there could be multiple oauth2:resource definitions, each with their own distinct pre-established-redirect-uri. So, there either needs to be a filter for each oauth2:resource, or a single filter aware of all oauth2:resource beans that is capable of finding the one matching a given redirect URI. It's not a trivial problem to solve.

david_syer on Mon, 7 Oct 2013 19:18:44 +0100

I played around a bit adding a registered redirect to a tonr client and it seems to work as I expected. Granted you have to be using OAuth2RestTemplate, but if you are using Spring OAuth2 on the client side and you are not using OAuth2RestTemplate then what are you doing exactly? If we can discuss this a bit without veering off into rant mode, then we might be able to agree on a way forward. (Setting this issue back to "New Feature" because I can't see yet why it is a bug.)

guitarking117 on Mon, 7 Oct 2013 19:49:23 +0100

Well, I am using OAuth2RestTemplate. But I'm not sure I'm explaining myself well.

Let's say I have three URIs, and these three URIs all use the OAuth2RestTemplate to access protected resources on the same resource server. The three URIs are /clientApp/foo, /clientApp/bar, and /clientApp/baz. The provider, however, only supports one registered redirect URI. Because the resource owner may access the three URIs in any order, there isn't one URI that will always work for the registered redirect URI.

So, in such a circumstance I would need a URI that can [A] serve as a redirect endpoint, exchanging the authorization code for the access token behind the scenes, and [B] redirect the user back to either /clientApp/foo, /clientApp/bar, or /clientApp/baz (whichever one they entered through). [A] should "just work", but doesn't, and so is a bug. [B] I can understand being classified a new feature, but one that is glaringly missing and naturally belongs as part of [A], IMO.

So, continuing this example, I choose a redirect URI /clientApp/oauth/confirm and set it up as the pre-established-redirect-uri. After going to /clientApp/foo, /clientApp/bar, or /clientApp/baz, I get redirected to the authorization server, where I grant access. The authorization server then sends me back to /clientApp/oauth/confirm with the authorization code. At this point two things should happen:

  1. There should be a filter that detects the request to /clientApp/oauth/confirm and exchanges the authorization code for an access token. This does not exist. That smells like a bug to me, and is not easily achieved with application code without making an unnecessary request to a random protected resource.
  2. After exchanging the authorization code for an access token, the filter should redirect the user back to the original request that started the whole process. This seems to naturally fit with the first part, but I understand calling this one part a new feature, since it is fairly easily accomplished with application code.

Another use case for this is to keep the code and state parameters essentially invisible to the user. By setting up a pre-established-redirect-uri that then redirects the user back to the original target, the code and state parameters only appear in the address bar for a brief instant before disappearing. Additionally, that way if the user hits "reload," the parameters aren't there in subsequent requests.

Now here's the important point I want to stress as an argument for classifying this as a bug. Other URIs in Spring Security OAuth 2 (for example, the /oauth/token and /oauth/authorize endpoints) "just work." Merely by enabling Spring Security OAuth 2, requests to these URIs are intercepted and handled appropriately. However, this is not the case with pre-established-redirect-uri. If you use pre-established-redirect-uri, it doesn't "just work." Instead, it "just breaks." The behavior exhibited is completely counterintuitive (and, to compound the problem, the documentation doesn't explain what to expect when using pre-established-redirect-uri). At the very least, I would (and did) expect requests to the pre-established-redirect-uri to exchange the authorization code for the access token, but they do not. The only thing pre-established-redirect-uri does is influence what redirect parameter the client sends to the authorization server on the authorization endpoint request. It sets up no mechanism for exchanging authorization codes for access tokens. Because of this, when the authorization server redirects the resource owner back to the redirect URI, nothing happens. The application never gets an access token like it normally would, and so the resource owner has to authorize all over again as soon as they go to one of the three original URIs.

Hopefully I've explained this a little more clearly now.

guitarking117 on Tue, 8 Oct 2013 06:36:23 +0100

Also, one other note that I should have mentioned in my comment immediately above. My example there covers OAuth2RestTemplate, as you expected. However, to respond to your question:

...if you are using Spring OAuth2 on the client side and you are not using OAuth2RestTemplate then what are you doing exactly?

OAuth is not a REST-exclusive technology. It is an authentication/authorization framework that is completely independent of the resource it is securing. These resources could be any number of things: RESTful web services, images, simple documents, or even _SOAP web services_ (yes, I said it, I would never use SOAP personally, but it can certainly be protected with OAuth). My specific use case is for RESTful web services, but that is not the only use case for OAuth. If I were going to support SOAP, I would secure it with OAuth. And if I were to write a client accessing SOAP web services or simple document/image resources, I would not use OAuth2RestTemplate. IMO, the process of detecting the authorization code and exchanging it for an access token should not be coupled to the OAuth2RestTemplate like it is now. It should be completely independent of anything RESTful or otherwise technology-specific.

In support of this argument, I would point out that there are separate XML namespace elements for oauth2:resource and oauth2:rest-template. However, because of the tight coupling, oauth2:resource is useless without oauth2:rest-template.

jibranbala commented 9 years ago

@dsyer Hi Dave, I recently started using OAuth2 in my app to integrate 3rd party apps like salesforce, google, facebook. I ran through this issue and with lots of research i landed here. The spring security Oauth2 is documented poorly as compared to other spring modules. I always think using spring is too easy because of its thorough documentation, but not in this case. Ok let me present my case. I am using salesforce as provider, i have a pre-established uri with it. When i make use-current-uri to true, salesforce sends me error of redirect-uri mismatch. I now used pre-established URI and handled it with controller. I have written only one line there, to get AccessToken. After provider hits this controler, getting accessToken uses AuthorisationCodeProvider and RestTemplate to use code and state parameter and again redirects to provider again for accessToken. The provider successfully sends accessToken, refreshToken in response and hits back to pre-established-URI.

Can u guide me through how to now redirect to actual client url from this pre-establish-uri which started authorization. One solution i am thinking is saving the current URI in session and then retrieve in pre-established-uri. The drawback for this is that i have to save current uri in all cases which uses protected resources. If this issue has been solved in new version, kindly help me.

jibranbala commented 9 years ago

@guitarking117 @Nick Williams Do anyone of you find the solution. I am new to implementing Oauth2 and i am facing the similar problem. I am not able to understand if i have to handle the callback URI with controller in my client app, how to implement this controller. There would be two cases in this controller: A- To get access token details if the request contains code parameter and B- To redirect to the client url which actually started oauth2 dance. @dsyer

dsyer commented 9 years ago

Does Salesforce really only support redirect to a single URL (most providers allow any URL with a common root)? If so I can see that recovering the original request when the OAuth dance is complete could be challenging. I expect you could leverage Spring Security (since it remembers original requests from before authentication): if you treat the token acquisition as an authentication from the point of view of Spring Security that would be trivial. New Spring Boot (1.3) features set most of it up for you if you declare that you want @EnableOAuth2Sso, but the pieces are all there in Spring Security OAuth2 (based on OAuth2ClientAuthenticationProcessingFilter).

jibranbala commented 9 years ago

@dsyer I cannot move to java configuration as my project is using xml configurations.with 2.5 container. What is the solution?, I am stuck with this for long?

And one more qs. I also use other third party integrations. Do u mean i dont need to handle pre-established-uri with controller. Do i need to use use-current-uri to true always? If so, what is the fun of establishing redirect uri with provider? Why is not to establish then only root of my app.???

dsyer commented 9 years ago

I don't think XML will hold you back particularly. You definitely need to register a redirect URL with any sensible provider, it's just that most of them will redirect to anything you ask them to, as long as it starts with the registered value (so paths within your top level resource always work). That's why useCurentUri defaults to "true" (it works out of the box for most providers).

jibranbala commented 9 years ago

@dsyer Ok. i registered localhost:7030/oauth/v0/callback as pre-established uri. Will the provider redirect me to url : localhost:7030/oauth/v0/contacts if current-uri set to true?? Or do i need to use localhost:7030/oauth/v0 as pre-establish-uri so that i can use child uri then only as redirects i.e. localhost:7030/oauth/v0/**?

Thanks for ur time and support :) :+1:

dsyer commented 9 years ago

It depends on the provider. With most of them you could register localhost:7030 and it would redirect to anything on that host/port. Usually it's a "starts with" pattern. If you register localhost:7030/oauth/v0 that normally means you can only redirect to localhost:7030/oauth/v0/**.

srt commented 8 years ago

This still is an issue at least when using Google resources. Google checks agains the full URL and does not use a "starts with" pattern.

mljohns89 commented 6 years ago

Has there been any update on this issue? It's almost 4 years since it was opened and here I am in the future and it's still bothering me :)

mljohns89 commented 6 years ago

So this was a gigantic pain that took me hours and hours of debugging and stepping through, but I finally found a solution. I'll spare everyone the amount of "debug" code I had to write to get to this and just post the solution for anyone else trying to figure this out.

Long story short, when you @EnableOAuth2Sso, it will return the access token in the SecurityContext.

Example:

@GetMapping("/hello")
public String helloUser(){
    OAuth2AuthenticationDetails authenticationDetails = 
            (OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails();
    String accessToken = authenticationDetails.getTokenValue();
    return accessToken;
    //or do some call to some ResourceServer with the access token.
}

I sincerely hopes this helps someone out there. I definitely think this could have been better documented. If I missed it some place obvious, hopefully someone will reply to this thread with the link :)

EDIT: For anyone curious, I made a demo project that demonstrates the Authorization Code Grant Flow using three separate projects for Auth Server, Client Web App and Resource API.

SammyJumpstart23 commented 6 years ago

@dsyer I am going to revive this thread because I'm facing the same problem. We're trying to access the API of a third-party provider but they only allow one pre-established redirect URL. I'm hoping that support for that will come sooner or later. Thank you very much. :D