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
toApplicationCommand
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.
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:
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:
package com.yourapp
class User {
// ...
static hasMany = [oAuthIDs: OAuthID]
}
4. Extensions
List of known extensions
4.1. How to create a new provider plugin?
-
Create a new plugin with command
grails create-plugin spring-security-oauth2-myProvider
. -
Add the following plugins as dependency in the build:
dependencies {
// ...
api 'org.grails.plugins:spring-security-core'
api 'org.grails.plugins:spring-security-oauth2'
}
-
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
-
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.
-
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.
-
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) }
-
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])
}