Skip to content

Commit

Permalink
Merge branch 'release/1.5.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
alvarosanchez committed Nov 20, 2015
2 parents 0933b01 + c6da3de commit 271f89f
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 36 deletions.
5 changes: 4 additions & 1 deletion SpringSecurityRestGrailsPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import grails.plugin.springsecurity.rest.authentication.DefaultRestAuthenticatio
import grails.plugin.springsecurity.rest.authentication.NullRestAuthenticationEventPublisher
import grails.plugin.springsecurity.rest.credentials.DefaultJsonPayloadCredentialsExtractor
import grails.plugin.springsecurity.rest.credentials.RequestParamsCredentialsExtractor
import grails.plugin.springsecurity.rest.error.DefaultCallbackErrorHandler
import grails.plugin.springsecurity.rest.oauth.DefaultOauthUserDetailsService
import grails.plugin.springsecurity.rest.token.bearer.BearerTokenAccessDeniedHandler
import grails.plugin.springsecurity.rest.token.bearer.BearerTokenAuthenticationEntryPoint
Expand Down Expand Up @@ -33,7 +34,7 @@ import javax.servlet.http.HttpServletResponse

class SpringSecurityRestGrailsPlugin {

String version = "1.5.2"
String version = "1.5.3"
String grailsVersion = "2.0 > *"
List loadAfter = ['springSecurityCore']
List pluginExcludes = [
Expand Down Expand Up @@ -177,6 +178,8 @@ class SpringSecurityRestGrailsPlugin {
/* tokenGenerator */
tokenGenerator(SecureRandomTokenGenerator)

callbackErrorHandler(DefaultCallbackErrorHandler)

/* tokenStorageService */
if (conf.rest.token.storage.useMemcached) {
conf.rest.token.storage.useJwt = false
Expand Down
4 changes: 3 additions & 1 deletion grails-app/conf/BuildConfig.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ grails.project.dependency.resolution = {
compile 'com.google.guava:guava-io:r03'
compile 'org.pac4j:pac4j-core:1.6.0'
compile 'org.pac4j:pac4j-oauth:1.6.0'
compile 'org.pac4j:pac4j-cas:1.6.0'
compile 'org.pac4j:pac4j-cas:1.6.0', {
exclude "bcprov-jdk15"
}
compile 'com.nimbusds:nimbus-jose-jwt:3.9'

build("com.lowagie:itext:2.0.8") { excludes "bouncycastle:bcprov-jdk14:138", "org.bouncycastle:bcprov-jdk14:1.38" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package grails.plugin.springsecurity.rest

import grails.plugin.springsecurity.annotation.Secured
import grails.plugin.springsecurity.rest.error.CallbackErrorHandler
import grails.plugin.springsecurity.rest.token.AccessToken
import grails.plugin.springsecurity.rest.token.rendering.AccessTokenJsonRenderer
import grails.plugin.springsecurity.rest.token.storage.TokenStorageService
Expand All @@ -39,6 +40,7 @@ class RestOauthController {

final String CALLBACK_ATTR = "spring-security-rest-callback"

CallbackErrorHandler callbackErrorHandler
RestOauthService restOauthService
GrailsApplication grailsApplication

Expand Down Expand Up @@ -89,36 +91,28 @@ class RestOauthController {

try {
String tokenValue = restOauthService.storeAuthentication(provider, context)

if (session[CALLBACK_ATTR]) {
frontendCallbackUrl += tokenValue
session[CALLBACK_ATTR] = null
} else {
frontendCallbackUrl = frontendCallbackUrl.call(tokenValue)
}
frontendCallbackUrl = getCallbackUrl(frontendCallbackUrl, tokenValue)

} catch (Exception e) {
String errorParams

if (e instanceof UsernameNotFoundException) {
errorParams = "&error=403&message=${e.message?.encodeAsURL()?:''}"
} else {
errorParams = "&error=${e.cause?.code?:500}&message=${e.message?.encodeAsURL()?:''}"
}
def errorParams = new StringBuilder()

if (session[CALLBACK_ATTR]) {
frontendCallbackUrl += errorParams
session[CALLBACK_ATTR] = null
} else {
frontendCallbackUrl = frontendCallbackUrl.call(errorParams)
Map params = callbackErrorHandler.convert(e)
params.each { key, value ->
errorParams << "&${key}=${value.encodeAsURL()}"
}

frontendCallbackUrl = getCallbackUrl(frontendCallbackUrl, errorParams.toString())
}

log.debug "Redirecting to ${frontendCallbackUrl}"
redirect url: frontendCallbackUrl
}

private String getCallbackUrl(baseUrl, String queryStringSuffix) {
session[CALLBACK_ATTR] = null
baseUrl instanceof Closure ? baseUrl(queryStringSuffix) : baseUrl + queryStringSuffix
}

/**
* Generates a new access token given the refresh token passed
*/
Expand Down
2 changes: 2 additions & 0 deletions src/docs/guide/introduction.gdoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ h4. Release History

You can view all releases at https://github.com/alvarosanchez/grails-spring-security-rest/releases.

* 20 November 2015
** [1.5.3|https://github.com/alvarosanchez/grails-spring-security-rest/issues?q=milestone%3A1.5.3+is%3Aclosed]
* 19 August 2015
** [1.5.2|https://github.com/alvarosanchez/grails-spring-security-rest/issues?q=milestone%3A1.5.2+is%3Aclosed]
* 6 May 2015
Expand Down
59 changes: 48 additions & 11 deletions src/docs/guide/oauth.gdoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,40 @@ can use any OAuth 2.0 provider they support. This includes at the time of writin

Note that OAuth 1.0a providers also work, like Twitter.

If your provider is not supported by pac4j, you can write your own. Please refer to the
[pac4j documentation|https://github.com/pac4j/pac4j/wiki/Clients#creating-your-own-client] for that.

The plugin also supports [CAS (Central Authentication Service)|https://www.apereo.org/projects/cas] using the OAuth
authentication flow. See [CAS Authentication|guide:oauth:cas] for details.
authentication flow. See [CAS Authentication|guide:cas] for details.

To start the OAuth authentication flow, from your frontend application, generate a link to
@<YOUR_GRAILS_APP>/oauth/authenticate/<provider>@. The user clicking on that link represents step 4 in the previous
diagram.

Note that you can define the frontend callback URL in @Config.groovy@ under
@grails.plugin.springsecurity.rest.oauth.frontendCallbackUrl@. You need to define a closure that will be called with
the token value as parameter:
@grails.plugin.springsecurity.rest.oauth.frontendCallbackUrl@.

{code}
grails.plugin.springsecurity.rest.oauth.frontendCallbackUrl = { String tokenValue -> "http://my.frontend-app.com/welcome#token=${tokenValue}" }
grails.plugin.springsecurity.rest.oauth.frontendCallbackUrl = "http://my.frontend-app.com/welcome#token="
{code}

The token will be concatenated to the end of the URL.

{note}
For backwards compatibility with versions 1.5.2 and earlier, the callback URL can also be defined as a closure that will
be called with the token value as a parameter:

{code}
grails.plugin.springsecurity.rest.oauth.frontendCallbackUrl = { String tokenValue -> "http://my.frontend-app.com/welcome#token=${tokenValue}" }
{code}
{note}

You can also define the URL as a @callback@ parameter in the original link, eg:

{code}
http://your-grails-api.com/oauth/authenticate/google?callback=http://your-frontend-app.com/auth-success.html?token=
{code}

In this case, the token will be *concatenated* to the end of the URL.

Upon successful OAuth authorisation (after step 6.1 in the above diagram), an
[OauthUser|http://alvarosanchez.github.io/grails-spring-security-rest/latest/docs/gapi/grails/plugin/springsecurity/rest/oauth/OauthUser.html]
will be stored in the security context. This is done by a bean named @oauthUserDetailsService@. The
Expand Down Expand Up @@ -127,12 +138,38 @@ OauthUser loadUserByUserProfile(OAuth20Profile userProfile, Collection<GrantedAu
}
{code}

In case of any OAuth authentication failure, the plugin will redirect back to the frontend application anyway, so it
has a chance to render a proper error message and/or offer the user the option to try again. In that case, the token
parameter will be empty, and both @error@ and @message@ params will be appended:
h3. Error Handling

In case of any OAuth authentication failure, the plugin will redirect back to the frontend application so it
can render an error message and/or offer the user the option to try again. When an error occurs, the token
parameter will be empty, and error parameters will be appended to the callback URL e.g.

{code}
http://your-frontend-app.com/auth-success.html?token=&error=403&message=User+with+email+jimmy%40gmail.com+now+allowed&error_description=User+with+email+jimmy%40gmail.com+not+allowed&error_code=UsernameNotFoundException
{code}

A description of each error parameter is given below

* @error@ - a code describing the error. This parameter is required by [RFC 6749|https://tools.ietf.org/html/rfc6749]
* @error_description@ - a human readable message describing the error. This parameter is optional according to [RFC 6749|https://tools.ietf.org/html/rfc6749]
* @message@ - the value of this parameter will be identical to @error_description@, it is provided only for backwards compatability with versions 1.5.2 and earlier which omitted @error_description@
* @error_code@ - the value of this parameter will be simple name of the exception that caused the error

The error handling described above is implemented by a Spring bean named @callbackErrorHandler@. This behaviour can be customised
by providing a replacement bean of the same name that implements this interface

{code}
http://your-frontend-app.com/auth-success.html?token=&error=403&message=User+with+email+jimmy%40gmail.com+now+allowed.+Only+%40example.com+accounts+are+allowed
interface CallbackErrorHandler {

/**
* Converts an error that occurs during the callback to a parameter map that will be returned to the frontend
* @param cause
* @return
*/
Map convert(Exception cause)
}
{code}

Below are some examples on how to configure it for Google, Facebook and Twitter.
It is strongly recommended (but not enforced by the plugin) that you include in the returned @Map@ any parameters required by [RFC 6749|https://tools.ietf.org/html/rfc6749]

The following sections provide examples of how to configure the plugin for usage with some popular OAuth providers: Google, Facebook and Twitter.
2 changes: 1 addition & 1 deletion src/docs/guide/whatsNew15.gdoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ Make sure to double-check your imports when upgrading to 1.5.
h2. Other minor changes

* The plugin now uses Grails 2.4.4, and the build and tests are executed with Java 8.
* Documentation for older versions is now published at [http://alvarosanchez.github.io/grails-spring-security-rest]
* Documentation for older versions is now published [here|http://alvarosanchez.github.io/grails-spring-security-rest].
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2013-2015 Alvaro Sanchez-Mariscal <alvaro.sanchezmariscal@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package grails.plugin.springsecurity.rest.error

interface CallbackErrorHandler {

/**
* Converts an error that occurs during the callback to a parameter map that will be returned to the frontend
* @param cause
* @return
*
* @author Dónal Murtagh
*/
Map convert(Exception cause)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2013-2015 Alvaro Sanchez-Mariscal <alvaro.sanchezmariscal@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package grails.plugin.springsecurity.rest.error

import org.springframework.security.core.userdetails.UsernameNotFoundException

import static org.springframework.http.HttpStatus.FORBIDDEN
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR

/**
* Error handler that's backwardly compatible with the behaviour that was embedded within the callback action of
* RestOauthController up to and including version 1.5.2
*
* @author Dónal Murtagh
*/
class DefaultCallbackErrorHandler implements CallbackErrorHandler {

@Override
Map convert(Exception e) {

Map params = [:]

if (e instanceof UsernameNotFoundException) {
params.error = FORBIDDEN.value()

} else {
params.error = e.cause?.hasProperty('code') ? e.cause.code : INTERNAL_SERVER_ERROR.value()
}

// Add the error message under the keys 'error_description' and 'message' - the former for compatibility with
// the RFC and the latter for backwards compatibility with plugin versions <= 1.5.2
params.error_description = params.message = e.message ?: ''
params.error_code = e.cause ? e.cause.class.simpleName : e.class.simpleName
params
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ class BearerTokenReader implements TokenReader {
AccessToken findToken(HttpServletRequest request) {
log.debug "Looking for bearer token in Authorization header, query string or Form-Encoded body parameter"
String tokenValue = null
String header = request.getHeader('Authorization')

if (request.getHeader('Authorization')?.startsWith('Bearer')) {
if (header?.startsWith('Bearer') && header.length()>=8) {
log.debug "Found bearer token in Authorization header"
tokenValue = request.getHeader('Authorization').substring(7)
} else if (isFormEncoded(request) && request.parts.size() <= 1 && !request.get) {
tokenValue = header.substring(7)
} else if (isFormEncoded(request) && !request.get) {
log.debug "Found bearer token in request body"
tokenValue = request.parameterMap['access_token']?.first()
} else if (request.queryString?.contains('access_token')) {
Expand Down
Loading

0 comments on commit 271f89f

Please sign in to comment.