1. Introduction to the Spring Security OAuth2 Plugin

The OAuth2 Plugin adds oAuth v2 sign-on support to Grails applications that use Spring Security. It depends on the Spring Security Core plugin.

It comes with a number of preconfigured OAuth2 providers, but you can implement any provider you like, that is extending the Scribejava DefaultApi20

1.1. Release History

Please read release notes at https://github.com/grails/grails-spring-security-oauth2/releases for checking release information in the future.

  • Version 3.0.0

    • Upgraded to Spring Security Core 6.1.0

    • Upgraded to Grails Gradle Plugin 6.1.0

    • Upgrade Script init-oauth2 to ApplicationCommand to support Grails 6.

  • Version 2.0.1

    • Update plugin com.gradle.common-custom-user-data-gradle-plugin to v1.12

  • Version 2.0.0

    • Upgraded to Grails 5.3 with Groovy 3..0.11

    • Fixed conflict with logback-config library.

  • Version 2.0.0-RC1

    • Upgraded to Grails 5.1

  • Version 1.0.0

    • released

2. Installation

Add the following dependencies in build.gradle:

build.gradle

dependencies {
    implementation "org.grails.plugins:spring-security-oauth2:{projectVersion}"
}

You will also need at least one provider extension, i.e the grails-spring-security-oauth2-google plugin.

3. Configuration

You can configure the following parameters in your application.yml.

Table 1. Plugin Configuration Options
Key Default Optional Description

grails.plugin.springsecurity.oauth2.active

true

true

Whether the plugin is active or not

grails.plugin.springsecurity.oauth2.registration.askToLinkOrCreateAccountUri

/oauth2/ask

true

The URI is called to ask the user to either create a new account or link an existing

grails.plugin.springsecurity.oauth2.registration.roleNames

['ROLE_USER']

true

A list of role names that should be automatically granted to OAuth User. There roles will be created if they do not exists

Here is an example configuration file:

application.yml
grails:
  plugin:
    springsecurity:
      oauth2:
        active: true
        registration:
          askToLinkOrCreateAccountUri: /oauth2/ask
          roleNames: ['ROLE_USER']

3.1. Initializing

Once you have a User domain class, initialize this plugin by using the init script

./gradlew runCommand "-Pargs=init-oauth2 [DOMAIN-CLASS-PACKAGE] [USER-CLASS-NAME] [OAUTH-ID-CLASS-NAME]"

The above command will create the domain class with the provided name and package. For example, the command ./gradlew runCommand "-Pargs=init-oauth2 com.yourapp User OAuthID will create a domain class OAuthID.groovy.

Finally, you also need to add the following domain class relationship mapping to the User.groovy domain class:

grails-app/domain/com/yourapp/User.groovy
package com.yourapp

class User {

    // ...

    static hasMany = [oAuthIDs: OAuthID]

}

4. Extensions

List of known extensions

4.1. How to create a new provider plugin?

  1. Create a new plugin with command grails create-plugin spring-security-oauth2-myProvider.

  2. Add the following plugins as dependency in the build:

build.gradle
dependencies {
    // ...
    api 'org.grails.plugins:spring-security-core'
    api 'org.grails.plugins:spring-security-oauth2'
}
  1. Create a Groovy class that extends OAuth2SpringToken and implement the abstract methods.

getProviderName // gets the provider name
getSocialId // is usually used for the  username
getScreenName // is usually used for the email address
  1. You may want to check if the scribe library has a default API built-in for your provider or create a Groovy class that extends DefaultApi20 and implement the abstract methods.

getAccessTokenEndpoint  // I would get this from config which is <providers domain>/oauth2/token
getAuthorizationBaseUrl // I would get this from get this from config which is <providers domain>/oauth2/authorize
getAccessTokenExtractor // In some implementations the `OpenIdJsonTokenExtractor` is used.
  1. Create a service in your plugin that extends OAuth2AbstractProviderService and implement the abstract methods. You can override the other methods for fine-tuning if needed.

getProviderID // whatever you want to call your provider.
getApiClass // points to your API implementation
getProfileScope // comes from config <domain>/oauth2/userInfo
getScopeSeparator // from the implementation that I've see usually: " " is used.
createSpringAuthToken // parses the OAuth2AccessToken to get the email and id that could be used
                      // to look up the user and puts them in a OAuth2SpringToken.
                      // This is also a good place to validate the token either inline or calling a separate method *
  • There maybe some variability between providers based on what is in the claims, but should be similar to this:

@Value('${grails.plugin.springsecurity.oauth2.providers.your_provider.api_key}')
String appId

def rawResponse = new JsonSlurper().parseText(accessToken.rawResponse)
String encodedIdToken = rawResponse.id_token
List<String> encodedIdTokenSegments = encodedIdToken.split('\\.')

String payloadClaimsStr = new String(Base64Utils.decodeFromUrlSafeString(encodedIdTokenSegments[1]))
Map payloadClaims = new JsonSlurper().parseText(payloadClaimsStr) as Map

if (payloadClaims.aud != appId) {
    throw new IllegalStateException("ID Token rejected: token specified incorrect recipient ID ${payloadClaims.aud}")
}

Integer now = new Date().time / 1000 as Integer // UNIX timestamp

if (now < payloadClaims.nbf) {
    throw new IllegalStateException("ID Token rejected: token cannot be processed before ${payloadClaims.nbf}; current time is $now")
}

if (now >= payloadClaims.exp) {
    throw new IllegalStateException("ID Token rejected: token has expired")
}

if (now < payloadClaims.iat) {
    throw new IllegalStateException("ID Token rejected: token cannot be from the future!")
}

Validating the token is important to security to make sure that the application client id is the same as what you sent to, because it prevents people from using tokens from other providers to try to grain access to your system.

  1. You can register your implementation of OAuth2AbstractProviderService in the plugin groovy file or if you can inline in the registration in BootStrap.groovy

try {
    springSecurityOauth2BaseService.registerProvider(yourAuth2Service)
} catch (OAuth2Exception exception) {
    log.error("There was an oAuth2Exception Your provider has not been loaded", exception)
}
  1. In the app that uses any of the extensions you’ll want to set up a URL mapping like:

"/oauth2/$provider/success"(controller: 'login', action: 'oauth2Success')

Then at that endpoint you can handle the user lookup, setting the authentication and redirecting, something like:

def oauth2Success(String provider) {
    log.info "In oauth2Success with $provider"

    if (!provider) {
        log.warn "The Spring Security OAuth callback URL must include the 'provider' URL parameter"
        throw new OAuth2Exception("The Spring Security OAuth callback URL must include the 'provider' URL parameter")
    }

    def sessionKey = springSecurityOauth2BaseService.sessionKeyForAccessToken(provider)

    if (!session[sessionKey]) {
        log.warn "No OAuth token in the session for provider '${provider}' your provider might require MFA before logging in to this server."
        throw new OAuth2Exception("Authentication error for provider '${provider}' your provider might require MFA before logging in to this server.")
    }

    // Create the relevant authentication token and attempt to log in.
    OAuth2SpringToken oAuthToken = createAuthToken(provider, session[sessionKey])

    if (oAuthToken.principal instanceof GrailsUser) {
        //provide you're own getDefaultTargetUrl method to replace with a string.
        authenticateAndRedirect(oAuthToken, getDefaultTargetUrl())
    } else {
        // This OAuth account hasn't been registered against an internal
        // account yet. Give the oAuthID the opportunity to create a new
        // internal account or link to an existing one.
        session[SpringSecurityOAuth2Controller.SPRING_SECURITY_OAUTH_TOKEN] = oAuthToken

        def redirectUrl = springSecurityOauth2BaseService.getAskToLinkOrCreateAccountUri()

        if (!redirectUrl) {
            log.warn "grails.plugin.springsecurity.oauth.registration.askToLinkOrCreateAccountUri configuration option must be set"
            throw new OAuth2Exception('Internal error')
        }
        log.debug "Redirecting to askToLinkOrCreateAccountUri: ${redirectUrl}"
        redirect(redirectUrl instanceof Map ? redirectUrl : [uri: redirectUrl])
    }
}


private OAuth2SpringToken createAuthToken(String providerName, OAuth2AccessToken scribeToken) {
    def providerService = springSecurityOauth2BaseService.getProviderService(providerName)
    OAuth2SpringToken oAuthToken = providerService.createSpringAuthToken(scribeToken)


    def user

    if(loadByUserName){
        //provide your own security service or do a lookup manually.
        user = securityService.loadUserByUsername(oAuthToken.getSocialId())
    }

    if(loadByEmail) {
        //provide your own security service or do a lookup manually.
        user = securityService.loadUserByEmailAddress(oAuthToken.getScreenName())
    }

    if (user) {
        updateOAuthToken(oAuthToken, user)
    }

    return oAuthToken
}

private OAuth2SpringToken updateOAuthToken(OAuth2SpringToken oAuthToken, user) {
    oAuthToken.principal = user
    oAuthToken.authorities = user.authorities
    oAuthToken.authenticated = true

    return oAuthToken
}


protected void authenticateAndRedirect(OAuth2SpringToken oAuthToken, redirectUrl) {
    session.removeAttribute SpringSecurityOAuth2Controller.SPRING_SECURITY_OAUTH_TOKEN
    SecurityContextHolder.context.authentication = oAuthToken
    redirect(redirectUrl instanceof Map ? redirectUrl : [uri: redirectUrl])
}