bluesliverx / grails-spring-security-oauth2-provider

Grails Spring Security OAuth2 Provider Plugin
http://grails.org/plugin/spring-security-oauth2-provider
57 stars 58 forks source link

Support generation of authorization code via an API #89

Closed ghost closed 5 years ago

ghost commented 9 years ago

Background

I'm using the plugin to implement an OAuth provider in an application that consists of two components

Frontend App

The frontend app is a single-page app (SPA) and consists of HTML/CSS/JS only. It retrieves it's data via REST calls to the backend Grails app.

Backend App

The backend Grails app has the OAuth provider plugin installed and provides data to the frontend app. There is no view code in this app and it is deployed on a separate host than the frontend app.

The Spring Security REST plugin is also installed, and a JSON Web Token is added to each API request to identify authenticated users.

Problem

The difficulty with using the plugin as-is, is that it presumes the OAuth views will be rendered on the server-side by GSPs. In my case, this is contrary to how the app has been designed to work, specifically, the login, create account, forgot password, etc. views already exist in the frontend app, and we'd obviously like to re-use these for the OAuth views, rather than reimplement them as GSPs.

Proposal

When a client requests an access token, a better workflow for us is:

  1. If the user is not authenticated, they must first of all authenticate via our existing views
  2. Once the user has authenticated, show an OAuth approval dialog, which is implemented as a view in the frontend app (where all the other view code exists)
  3. If approval is granted, make an API request to the backend to generate an authorization code
  4. When the frontend app receives the authorization code it is forwarded to the client, which can exchange it for an access token with the backend app directly.

    Solution

The only change the plugin requires to support this proposal is the ability to generate an authorization code programatically. I've implemented the following service and it appears to work

import grails.plugin.springsecurity.SpringSecurityService
import org.springframework.security.oauth2.provider.AuthorizationRequest
import org.springframework.security.oauth2.provider.OAuth2Authentication
import org.springframework.security.oauth2.provider.OAuth2Request
import org.springframework.security.oauth2.provider.OAuth2RequestFactory
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices

class OAuthProviderService {

    OAuth2RequestFactory oauth2RequestFactory
    SpringSecurityService springSecurityService
    AuthorizationCodeServices authorizationCodeServices

    String generateAuthorizationCode(AuthorizationCodeRequest command) {

        // the command object should already have been validated (e.g in the controller)
        // by the time we get to this point   

        Map requestParams = [
                scope: command.scope,
                response_type: command.responseType,
                redirect_uri: command.redirectUri,
                client_id: command.clientId
        ]

        // create an object representing an approved request for an authorization code, i.e. after the user has
        // granted their approval via the consent dialog
        AuthorizationRequest approvedAuthCodeRequest = oauth2RequestFactory.createAuthorizationRequest(requestParams)
        approvedAuthCodeRequest.approved = true

        OAuth2Request storedOAuth2Request = oauth2RequestFactory.createOAuth2Request(approvedAuthCodeRequest)
        OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, springSecurityService.authentication)
        authorizationCodeServices.createAuthorizationCode(combinedAuth)
    }
}
import grails.validation.Validateable
import org.example.Client

@Validateable
class AuthorizationCodeRequest {
    String scope
    String responseType
    String redirectUri
    String clientId

    // these constraints perform much of the same validation logic as 
    // org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint
    // which would otherwise be omitted
    static constraints = {

        def getClient = {
            Client.findByClientId(it, [cache: true])
        }

        clientId validator: {
            getClient(it) != null
        }

        responseType inList: ["token", "code"]

        redirectUri validator: { uri, AuthorizationCodeRequest self ->

            Client client = getClient(self.clientId)

            if (client) {
                uri in client.redirectUris
            }
        }

        scope validator: { scope, AuthorizationCodeRequest self ->
            Client client = getClient(self.clientId)

            if (client) {
                scope in client.scopes
            }
        }
    }
}

Obviously this is not a complete solution, e.g. an endpoint also needs to be added that calls this service. Is this something that you'd consider adding to the plugin?

bobby-vandiver commented 9 years ago

It sounds like your proposal is at odds with how the authorization code flow is defined to work. The client (browser) can't be trusted and should not be taking part in any of the authorization-code-to-access-token exchange. The implicit grant flow is intended for clients that can't be trusted, but this doesn't exactly fit your use-case as I understand it.

I looked around and I came across this SO question that sounds very similar to what you're trying to accomplish, specificially this answer. Is that any help?

donalmurtagh commented 9 years ago

The client (browser) can't be trusted and should not be taking part in any of the authorization-code-to-access-token exchange.

The browser isn't involved in the authorization-code-to-access-token exchange. The only two parties involved in this are the Backend App (my Grails OAuth provider) and the OAuth client. I could possibly illustrate the actors and messages that are exchanged among them with a sequence diagram if you think that would make things clearer?

bobby-vandiver commented 9 years ago

I think that would help my understanding, if you don't mind throwing one together.

ghost commented 9 years ago

oauth-sequence

"Generate auth code" is the step wherein the authorization code is generated programmatically

bobby-vandiver commented 9 years ago

Could you clarify the roles of the "Front End App" and the "OAuth Client App". Are they the same? The first step "Click login button in OAuth client app" is targeted and the "Front End App" and not the "OAuth Client App".

The standard behavior of the authorization server is to perform a redirect to the client's registered URI post authorization. I don't see this step in your sequence diagram. Am I overlooking it?

ghost commented 9 years ago

Could you clarify the roles of the "Front End App" and the "OAuth Client App".

The OAuth provider app consists of two components: an Angular application that is responsible for rendering the view and a Grails app (with the OAuth provider plugin installed). The Angular app consists of HTML/CSS/JS only and it retrieves it's data via REST calls to the Grails app. In the diagram above, "Frond End App" refers to the Angular app and "OAuth Provider" refers to the Grails app. The "OAuth Client App" is a completely separate application which plays the role of the OAuth client. For testing purposes, I use this app as the OAuth client.

The standard behavior of the authorization server is to perform a redirect to the client's registered URI post authorization. I don't see this step in your sequence diagram. Am I overlooking it?

The step labelled "Auth code response" is the redirect from the provider to the client with the authorization code. The step labelled "Access token" is the redirect from the provider to the client with the access token.

bobby-vandiver commented 8 years ago

Adding a "Feature Request" label to this. I'm not opposed to adding a service that exposes this functionality if someone wants to put together a pull request for it. I wonder if there's anything in spring-security-oauth that we could leverage to make @domurtag's solution a more complete one.

bluesliverx commented 5 years ago

Migrated to the new project.