The OAuth1 plugin adds OAuth 1.0 support to a Grails application that uses Spring Security. It depends on Spring Security Core plugin.
This documentation specifies a few specific steps you will have to take in order to ensure proper integration with the underlying library.
This plugin provides support for Grails domain classes necessary for providing OAuth 1.0 authorization. Access to protected resources is controlled by a combination of Spring Security Core’s methods, i.e. request maps, annotations, intercept maps and careful configuration of the Spring Security filter chains.
The following assumes that the Spring Security Core plugin has been installed and its required domain class created.
Install the OAuth1 plugin by adding a dependency in grails-app/conf/BuildConfig.groovy
plugins {
compile ":spring-security-oauth1-provider:1.0"
}
There is following two domain classes required which needs to be create manually for now
packaage com.security.oauth
class OAuthConsumer {
String consumerKey
String consumerName
String consumerSecret
static hasMany = [ authorities: String ]
static constraints = {
consumerKey blank: false, unique: true
consumerSecret nullable: true
consumerName nullable: true
authorities nullable: true
}
}
packaage com.security.oauth
class OAuthAccessProviderTokenWrapper implements OAuthAccessProviderToken {
transient userAuthenticationtransient userAuthentication
String value
String callbackUrl
String verifier
String secret
String consumerKey
boolean accessToken
long timestamp = System.currentTimeMillis()
def user
static transients = ['userAuthentication']
static constraints = {
consumerKey blank: false value nullable: true
callbackUrl nullable: true
verifier nullable: true
secret nullable: true
timestamp nullable: true
user nullable:true
userAuthentication nullable:true
}
public void setUserAuthentication(Authentication userAuthentication) {
this.userAuthentication = userAuthentication;
}
@Override
public Authentication getUserAuthentication() {
return this.userAuthentication
}
}
Now provided these domains information after adding it in grails-app/conf/Config.groovy
:
grails.plugin.springsecurity.oauthProvider.consumerLookup.className = 'com.security.oauth.OAuthConsumer'
grails.plugin.springsecurity.oauthProvider.tokenLookup.className = 'com.security.oauth.OAuthAccessProviderTokenWrapper'
Update the Core plugin’s rules for the authorization and token endpoints so they are protected by Spring Security. If you’re using the Core plugin’s staticRules
, you’ll want to add the following in grails-app/conf/Config.groovy
:
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
'/oauth_request_token' : ["(request.getMethod().equals('GET') or request.getMethod().equals('POST'))"],
'/oauth_authenticate_token': ["(request.getMethod().equals('GET') or request.getMethod().equals('POST'))"],
... The endpoints are standard Spring MVC controllers in the underlying Spring Security OAuth1 implementations.
consumer_secret
From LogsUpdate the params exclusion list in grails-app/conf/Config.groovy
so client secrets are not logged in the clear:
grails.exceptionresolver.params.exclude = ['password', 'consumer_secret']
At this point your application is a proper OAuth 1.0 provider. You can now register consumers in what ever method is appropriate for your application. For example, you can register a consumer in grails-app/conf/Bootstrap.groovy
as follows:
def init = { servletContext ->
new OAuthConsumer
(consumerKey: 'my-consumer',
consumerName : 'Consumer Name',
consumerSecret : 'my-secret',
authorities : ['ROLE_CLIENT']).save(flush: true, failOnError: true)
}
Access to resources is controlled by the Spring Security Core plugin’s access control mechanisms. Additionally, the plugin has full support for the OAuth 1.0 extensions provided by the underlying Spring library.
class ConsumerController {
@Secured('ROLE_CLIENT')
def getConsumerInfo() {
if(params.oauth_consumer_key) {
render "It's oauth1 request"
} else {
render "it's original login request"
}
}
}
The filter chains must be configured to ensure stateless access to the token endpoint and any OAuth 1.0 resources:
grails.plugin.springsecurity.filterChain.chainMap = [
'/oauth_request_token' : 'oauthRequestTokenFilter',
'/oauth_authenticate_token' : 'securityRequestHolderFilter,securityContextPersistenceFilter,securityContextHolderAwareRequestFilter,rememberMeAuthenticationFilter,exceptionTranslationFilter,filterInvocationInterceptor,oauthAuthenticateTokenFilter',
'/oauth_access_token' : 'securityRequestHolderFilter,securityContextPersistenceFilter,oauthAccessTokenFilter',
'/consumer/**' : 'securityRequestHolderFilter,securityContextPersistenceFilter,oauthProtectedResourceFilter,rememberMeAuthenticationFilter,anonymousAuthenticationFilter,exceptionTranslationFilter,filterInvocationInterceptor',
'/**' : 'JOINED_FILTERS'
]
No default class name is assumed for the required domain classes. They must be specified in grails-app/conf/Config.groovy
as follows :
grails.plugin.springsecurity.oauthProvider.consumerLookup.className = 'com.security.oauth.OAuthConsumer'
grails.plugin.springsecurity.oauthProvider.tokenLookup.className = 'com.security.oauth.OAuthAccessProviderTokenWrapper'
The following properties exist in the grails.plugin.springsecurity.oauthProvider
namespace.
Property | Default Value | Meaning |
---|---|---|
consumerLookup.className | null | Consumer class name. |
consumerLookup.consumerKeyPropertyName | consumerKey | Consumer class consumer key field. |
consumerLookup.consumerNamePropertyName | consumerName | Consumer class consumer name field. |
consumerLookup.consumerSecretPropertyName | consumerSecret | Consumer class consumer secret field. |
consumerLookup.authoritiesPropertyName | authorities | Consumer class authorities field. |
Property | Default Value | Meaning |
---|---|---|
tokenLookup.callbackUrlPropertyName | callbackUrl | AccessProviderToken class call back URL field. |
tokenLookup.className | null | AccessProviderToken class name. |
tokenLookup.consumerKeyPropertyName | consumerKey | AccessProviderToken class consumer key field. |
tokenLookup.isAccessTokenPropertyName | accessToken | AccessProviderToken class to check accesstoken field |
tokenLookup.secretPropertyName | secret | AccessProviderToken class secret field. |
tokenLookup.timestampPropertyName | timestamp | AccessProviderToken class timestamp field. |
tokenLookup.userAuthenticationPropertyName | userAuthentication | AccessProviderToken class user authentication field. |
tokenLookup.userPropertyName | user | AccessProviderToken class user field. |
tokenLookup.valuePropertyName | value | AccessProviderToken class value field. |
tokenLookup.verifierPropertyName | verifier | AccessProviderToken class verifier field. |
The plugin is pessimistic by default, locking down as much as possible to guard against accidental security breaches. However, these constraints can be modified if so desired in grails-app/conf/Config.groovy
. The properties below exist in the grails.plugin.springsecurity.oauthProvider
namespace.
The following properties exist in the grails.plugin.springsecurity.oauthProvider namespace :
Property | Default Value | Meaning |
---|---|---|
require10a | true | To enable oauth1.0. |
Property | Default Value | Meaning |
---|---|---|
requestTokenFilter.filterProcessesUrl | /oauth_request_token | End point url for request token. |
requestTokenFilter.ignoreMissingCredentials | false | Ignore missing credential for request token flag. |
requestTokenFilter.allowedMethods | ['GET', 'POST'] | Methods to support for request. |
requestTokenFilter.responseContentType | text/plain;charset=utf-8 | Response content type to parse. |
Property | Default Value | Meaning |
---|---|---|
entryPoint.realmName | Grails OAuth Provider | Resposne end point handler realm. |
Property | Default Value | Meaning |
---|---|---|
nonce.validityWindowSeconds | 60 * 60 * 12 | Validitiy period of nonce. |
Property | Default Value | Meaning |
---|---|---|
provider.baseUrl | null | Provider base URL. |
Property | Default Value | Meaning |
---|---|---|
signature.supportPlainText | false | Support plain text. |
signature.supportHMAC_SHA1 | true | Support HMAC_SHA1 signature method. |
signature.supportRSA_SHA1 | true | Support RSA_SHA1 signature method. |
Property | Default Value | Meaning |
---|---|---|
tokenServices.accessTokenValiditySeconds | 60 * 60 * 12 | Acess token validity period. |
tokenServices.requestTokenValiditySeconds | 60 * 10 | Request token validity period. |
tokenServices.tokenSecretLengthBytes | 80 | Token secret length. |
Property | Default Value | Meaning |
---|---|---|
authTokenFilter.filterProcessesUrl | /oauth_authenticate_token | End point URL to authorize token. |
authTokenFilter.tokenIdParameterName | requestToken | Parameter name to get request token for authorization. |
Property | Default Value | Meaning |
---|---|---|
verifier.lengthBytes | 6 | Length of verifier request token. |
Property | Default Value | Meaning |
---|---|---|
successHandler.tokenIdParameterName | requestToken | Response token parameter name. |
successHandler.callbackParameterName | callbackURL | Response callback URL parameter name. |
Property | Default Value | Meaning |
---|---|---|
accessTokenFilter.filterProcessesUrl | /oauth_access_token | End point URL to obtain acesstoken. |
accessTokenFilter.ignoreMissingCredentials | false | Ignore missing credetial to obtain accesstoken flag. |
accessTokenFilter.allowedMethods | ['GET', 'POST'] | Methods to support forobtain accesstoken. |
Property | Default Value | Meaning |
---|---|---|
protectedResourceFilter.allowAllMethods | true | Allow all methods flag. |
protectedResourceFilter.ignoreMissingCredentials | true | Ignore missing credetial to acess resources flag. |
The key to understanding how OAuth1 works is understanding the authorization flow. This is the process clients go through to link to a site.
The flow with the OAuth1 plugin is called the three-legged flow, thanks to the three primary steps involved:
Temporary Credentials Acquisition: The client gets a set of temporary credentials from the server. Authorization: The user “authorizes” the request token to access their account. Token Exchange: The client exchanges the short-lived temporary credentials for a long-lived token.
The first step to authorization is acquiring temporary credentials (also known as a Request Token). These credentials are short-lived (typically 24 hours), and are used purely for the initial authorization process. They don’t grant any access to data on the server, and cannot be used for anything except the authorization flow.
These credentials are acquired by an initial HTTP request to the server. The client starts by sending a POST request to the temporary credential URL, typically /oauth_request_token
with the plugin. (This URL should be autodiscovered from the API, as individual sites may move this route, or delegate the process to another server.) This looks something like:
This request includes the client key (oauth_consumer_key
), the authorization callback (oauth_callback
), and the request signature (oauth_signature
and oauth_signature_method
). This looks something like:
POST /oauth_request_token HTTP/1.1
Host: http://localhost:8080
Authorization: OAuth realm="Example",
oauth_consumer_key="my-consumer",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1513678344",
oauth_nonce="Dc85Pk",
oauth_version=1.0,
oauth_callback="http://localhost:8080/token",
oauth_signature="zCVppwQZ0Y7T68gxPLFzaMObhks="
Note : You can directlly hit the following URL on browser for getting the same :
http://localhost:8080/oauth_request_token?oauth_consumer_key=my-consumer&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1513678344&oauth_nonce=Dc85Pk&oauth_version=1.0&oauth_signature=zCVppwQZ0Y7T68gxPLFzaMObhks=&oauth_callback=http://localhost:8080/token
The server checks the key and signature to ensure the client is valid. It also checks the callback to ensure it’s valid for the client.
Once the checks are complete, the server creates a new set of Temporary Credentials (oauth_token
and oauth_token_secret
) and returns them in the HTTP response (URL encoded). This looks something like:
HTTP/1.1 200 OK
Content-Type: application/x-www-form-urlencoded
oauth_token=hdk48Djdsa&oauth_token_secret=xyz4992k83j47x0b&oauth_callback_confirmed=true
These credentials are then used as the oauth_token and oauth_token_secret parameters for the Authorization and Token Exchange steps.
Note : The oauth_callback_confirmed=true
will always be returned, and indicates that the protocol is OAuth 1.0.
The next step in the flow is the authorization process. This is a user-facing step, and the one that most users will be familiar with.
Using the authorization URL supplied by the site (typically /oauth_authenticate_token
), the client appends the temporary credential key (requestToken
from above) to the URL as a query parameter (again as requestToken
). The client then directs the user to this URL. Typically, this is done via a redirect for in-browser clients, or opening a browser for native clients.
The user then logs in if they aren’t already, and authorizes the client. They can also choose to cancel the authorization process if they don’t want to link the client.
If the user authorizes the client, the site then marks the token as authorized, and redirects the user back to the callback URL. The callback URL includes two extra query parameters: oauth_token
(the same temporary credential token) and oauth_verifier
, a CSRF token that needs to be passed in the next step.
For eg. http://localhost:8080/token?oauth_token=b9c91204-e4c3-42b3-a479-b85dff86427d&oauth_verifier=uVA2W3 where http://localhost:8080/token is redirect URL
The final step in authorization is to exchange the temporary credentials (request token) for long-lived credentials (also known as an Access Token). This request also destroys the temporary credentials.
The temporary credentials are converted to long-lived credentials by sending a POST request to the token request endpoint (typically /oauth_authenticate_token
). This request must be signed by the temporary credentials, and must include the oauth_verifier token from the authorization step. The request looks something like:
POST /oauth_authenticate_token HTTP/1.1
Host: http://localhost:8080
Authorization: OAuth realm="Example",
oauth_consumer_key="my-consumer",
oauth_token="hdk48Djdsa",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1513668573",
oauth_nonce="8JpkZv", oauth_verifier="uVA2W3",
oauth_version=1.0,
oauth_signature="/133b9TEIp3f9l3W5hZVgEOyTYA="
Note : You can directly hit the following URL on browser for getting the same :
http://localhost:8080/oauth_access_token?oauth_consumer_key=my-consumer&oauth_token=b9c91204-e4c3-42b3-a479-b85dff86427d&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1513668573&oauth_nonce=8JpkZv&oauth_version=1.0&oauth_signature=/133b9TEIp3f9l3W5hZVgEOyTYA=&oauth_verifier=uVA2W3
The server again checks the key and signature, as well as also checking the verifier token to avoid CSRF attacks.
Assuming these checks all pass, the server will respond with the final set of credentials in the HTTP response body (form data, URL-encoded):
HTTP/1.1 200 OK
Content-Type: application/x-www-form-urlencoded
oauth_token=j49ddk933skd9dks&oauth_token_secret=ll399dj47dskfjdk
At this point, you can now discard the temporary credentials (as they are now useless), as well as the verifier token.