├── version.txt ├── settings.gradle ├── src ├── docs │ ├── images │ │ └── create_app.png │ ├── usage.adoc │ ├── index.adoc │ ├── introduction.adoc │ ├── configuration-fb-app.adoc │ ├── configuration-plugin.adoc │ ├── ref │ │ └── Tags │ │ │ ├── init.gdoc │ │ │ └── connect.gdoc │ ├── configuration.adoc │ ├── configuration-login-btn.adoc │ ├── configuration-domains.adoc │ ├── how-to.adoc │ ├── installation.adoc │ ├── common-issues.adoc │ ├── usage-client-side.adoc │ ├── usage-json.adoc │ ├── usage-server-side.adoc │ ├── usage-basic.adoc │ ├── configuration-types.adoc │ ├── usage-filters.adoc │ ├── index.tmpl │ └── customization.adoc ├── main │ ├── templates │ │ ├── facebookauth.js │ │ ├── FacebookUser.groovy.template │ │ └── FacebookAuthDaoImpl.groovy.template │ └── groovy │ │ └── com │ │ └── the6hours │ │ └── grails │ │ └── springsecurity │ │ └── facebook │ │ ├── FacebookUserDomain.groovy │ │ ├── FacebookAccessToken.groovy │ │ ├── InvalidCookieException.groovy │ │ ├── InvalidRequestException.groovy │ │ ├── DomainsRelation.groovy │ │ ├── FacebookAuthToken.groovy │ │ ├── FacebookAuthCookieDirectFilter.groovy │ │ ├── FacebookAuthJsonFilter.groovy │ │ ├── FacebookAuthDao.groovy │ │ ├── FacebookAuthCookieLogoutHandler.groovy │ │ ├── FacebookAuthRedirectFilter.groovy │ │ ├── FacebookAuthCookieTransparentFilter.groovy │ │ ├── JsonAuthenticationHandler.groovy │ │ ├── FacebookAuthProvider.groovy │ │ ├── FacebookAuthUtils.groovy │ │ ├── SpringSecurityFacebookGrailsPlugin.groovy │ │ └── DefaultFacebookAuthDao.groovy └── test │ └── groovy │ ├── log4j.properties │ └── com │ └── the6hours │ └── grails │ └── springsecurity │ └── facebook │ ├── TestRole.groovy │ ├── TestFacebookUser.groovy │ ├── TestAppUser.groovy │ ├── TestAuthority.groovy │ ├── FacebookAuthTokenSpec.groovy │ ├── FacebookAuthProviderSpec.groovy │ ├── FacebookAuthUtilsSpec.groovy │ └── DefaultFacebookAuthDaoSpec.groovy ├── grails-app ├── assets │ └── images │ │ └── connect.png ├── conf │ ├── UrlMappings.groovy │ ├── logback.groovy │ ├── DefaultFacebookSecurityConfig.groovy │ └── application.yml ├── init │ └── com │ │ └── the6hours │ │ └── grails │ │ └── springsecurity │ │ └── facebook │ │ └── Application.groovy └── taglib │ └── com │ └── the6hours │ └── grails │ └── springsecurity │ └── facebook │ └── FacebookAuthTagLib.groovy ├── .gitignore ├── gradle.properties ├── LICENSE.txt ├── README.markdown ├── gradle ├── common.gradle ├── grailsCentralPublishing.gradle ├── testapp.gradle └── plugin.gradle ├── gradlew.bat └── gradlew /version.txt: -------------------------------------------------------------------------------- 1 | 0.19.2 -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spring-security-facebook' -------------------------------------------------------------------------------- /src/docs/images/create_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splix/grails-spring-security-facebook/HEAD/src/docs/images/create_app.png -------------------------------------------------------------------------------- /src/main/templates/facebookauth.js: -------------------------------------------------------------------------------- 1 | function onFacebookLogin(sess) { 2 | 3 | } 4 | 5 | function onFacebookLogout(sess) { 6 | 7 | } -------------------------------------------------------------------------------- /grails-app/assets/images/connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splix/grails-spring-security-facebook/HEAD/grails-app/assets/images/connect.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | *.iml 4 | *.ipr 5 | *.iws 6 | *.zip 7 | target 8 | *.log 9 | .gradle 10 | build 11 | .idea 12 | cobertura.ser 13 | kindlegen -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | grailsVersion=3.2.8 2 | gradleWrapperVersion=2.4.10 3 | vcsUrl=https://github.com/splix/grails-spring-security-facebook 4 | userOrg=splix 5 | repo=grails-plugins 6 | 7 | -------------------------------------------------------------------------------- /src/docs/usage.adoc: -------------------------------------------------------------------------------- 1 | == Usage 2 | 3 | include::usage-basic.adoc[] 4 | 5 | include::usage-filters.adoc[] 6 | 7 | include::usage-server-side.adoc[] 8 | 9 | include::usage-client-side.adoc[] 10 | 11 | include::usage-json.adoc[] -------------------------------------------------------------------------------- /grails-app/conf/UrlMappings.groovy: -------------------------------------------------------------------------------- 1 | class UrlMappings { 2 | 3 | static mappings = { 4 | "/$controller/$action?/$id?"{ 5 | constraints { 6 | // apply constraints here 7 | } 8 | } 9 | 10 | "/"(view:"/index") 11 | "500"(view:'/error') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/groovy/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=WARN, stdout 2 | 3 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 4 | log4j.appender.stdout.Target=System.out 5 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 6 | log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{1}:%M:%L - %m%n 7 | 8 | log4j.logger.com.the6hours=DEBUG 9 | -------------------------------------------------------------------------------- /src/main/templates/FacebookUser.groovy.template: -------------------------------------------------------------------------------- 1 | ${domainPackageDeclaration} 2 | 3 | import ${userClassFullName} 4 | 5 | class ${domainClassName} { 6 | 7 | Long uid 8 | String accessToken 9 | Date accessTokenExpires 10 | 11 | static belongsTo = [user: ${userClassName}] 12 | 13 | static constraints = { 14 | uid unique: true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /grails-app/init/com/the6hours/grails/springsecurity/facebook/Application.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | 6 | class Application extends GrailsAutoConfiguration { 7 | static void main(String[] args) { 8 | GrailsApp.run(Application, args) 9 | } 10 | } -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookUserDomain.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @CompileStatic 6 | interface FacebookUserDomain { 7 | 8 | String getAccessToken() 9 | void setAccessToken(String accessToken) 10 | 11 | long getUid() 12 | void setUid(long uid) 13 | } 14 | -------------------------------------------------------------------------------- /src/docs/index.adoc: -------------------------------------------------------------------------------- 1 | = Spring Security Facebook Plugin - Reference Documentation 2 | Igor Artamonov 3 | 4 | :doctype: book 5 | :encoding: utf-8 6 | :lang: en 7 | :toc: left 8 | :toclevels: 2 9 | :numbered: 10 | 11 | include::introduction.adoc[] 12 | 13 | include::installation.adoc[] 14 | 15 | include::usage.adoc[] 16 | 17 | include::configuration.adoc[] 18 | 19 | include::customization.adoc[] 20 | 21 | include::how-to.adoc[] 22 | 23 | include::common-issues.adoc[] -------------------------------------------------------------------------------- /src/test/groovy/com/the6hours/grails/springsecurity/facebook/TestRole.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | /** 4 | * 5 | * Since 23.04.13 6 | * @author Igor Artamonov, http://igorartamonov.com 7 | */ 8 | class TestRole { 9 | 10 | static List _calls = [] 11 | 12 | static def withTransaction(Closure c) { 13 | return c.call() 14 | } 15 | 16 | static void create(def appUser, def auth) { 17 | _calls << ['create', [appUser, auth]] 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/docs/introduction.adoc: -------------------------------------------------------------------------------- 1 | == Introduction 2 | 3 | === Requirements 4 | 5 | * Grails 3.0 6 | * spring-security-core plugin 3.0.0 7 | 8 | === Links 9 | 10 | * Sources - https://github.com/splix/grails-spring-security-facebook 11 | * Sample App - https://github.com/splix/grails-facebook-authentication-example 12 | * if you found a bug - please submit it into https://github.com/splix/grails-spring-security-facebook/issues 13 | * other questions please ask at StackOverflow - http://stackoverflow.com/questions/tagged/grails -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAccessToken.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | /** 6 | * 7 | * @author Igor Artamonov (http://igorartamonov.com) 8 | * @since 22.05.12 9 | */ 10 | @CompileStatic 11 | class FacebookAccessToken implements Serializable { 12 | 13 | String accessToken 14 | Date expireAt 15 | 16 | String toString() { 17 | 'Access token: ' + accessToken + ', expires at ' + expireAt 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/InvalidCookieException.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import org.springframework.security.core.AuthenticationException 6 | 7 | /** 8 | * 9 | * @author Igor Artamonov (http://igorartamonov.com) 10 | * @since 05.07.12 11 | */ 12 | @CompileStatic 13 | class InvalidCookieException extends AuthenticationException { 14 | 15 | InvalidCookieException(String msg) { 16 | super(msg) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/groovy/com/the6hours/grails/springsecurity/facebook/TestFacebookUser.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | /** 4 | * 5 | * Created at 20.04.13 6 | * @author Igor Artamonov (http://igorartamonov.com) 7 | */ 8 | class TestFacebookUser { 9 | 10 | static _calls = [] 11 | 12 | Long uid 13 | String accessToken 14 | Date accessTokenExpires 15 | 16 | TestAppUser user 17 | 18 | static withTransaction(Closure c) { 19 | c.call() 20 | } 21 | 22 | def save(args) { 23 | _calls << ['save', args] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2011 Igor Artamonov 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/test/groovy/com/the6hours/grails/springsecurity/facebook/TestAppUser.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | /** 4 | * 5 | * Created at 20.04.13 6 | * @author Igor Artamonov (http://igorartamonov.com) 7 | */ 8 | class TestAppUser { 9 | 10 | static _calls = [] 11 | 12 | String username 13 | String password 14 | boolean enabled 15 | boolean expired 16 | boolean locked 17 | boolean passwordExpired 18 | 19 | static def withTransaction(Closure c) { 20 | return c.call() 21 | } 22 | 23 | def save(def args) { 24 | _calls << ['save', args] 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/InvalidRequestException.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import org.springframework.security.core.AuthenticationException 6 | 7 | /** 8 | * 9 | * @author Igor Artamonov (http://igorartamonov.com) 10 | * @since 19.09.12 11 | */ 12 | @CompileStatic 13 | class InvalidRequestException extends AuthenticationException { 14 | 15 | InvalidRequestException(String msg, Throwable t) { 16 | super(msg, t) 17 | } 18 | 19 | InvalidRequestException(String msg) { 20 | super(msg) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/groovy/com/the6hours/grails/springsecurity/facebook/TestAuthority.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | /** 4 | * 5 | * Since 23.04.13 6 | * @author Igor Artamonov, http://igorartamonov.com 7 | */ 8 | class TestAuthority { 9 | 10 | static _calls = [] 11 | 12 | String name 13 | 14 | static TestAuthority findByName(String name) { 15 | _calls << ['findByName', name] 16 | if (name == 'ROLE_USER') { 17 | return new TestAuthority(name: 'ROLE_USER') 18 | } 19 | return null 20 | } 21 | 22 | static def withTransaction(Closure c) { 23 | return c.call() 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/docs/configuration-fb-app.adoc: -------------------------------------------------------------------------------- 1 | === Configure Facebook App 2 | 3 | |====== 4 | | *Name* | *Default Value* 5 | | grails.plugin.springsecurity.facebook.secret | must be specified 6 | | grails.plugin.springsecurity.facebook.appId | must be specified 7 | | grails.plugin.springsecurity.facebook.apiVersion | not set 8 | |====== 9 | 10 | * `apiVersion` - Facebook API version (e.g., "v2.2"). If not set is used unversioned Facebook API by default. 11 | 12 | |====== 13 | | *Name* | *Default Value* 14 | | grails.plugin.springsecurity.facebook.permissions | ['email'] 15 | |====== 16 | 17 | For a list of all possible permissions see https://developers.facebook.com/docs/reference/login/#permissions 18 | -------------------------------------------------------------------------------- /grails-app/conf/logback.groovy: -------------------------------------------------------------------------------- 1 | import grails.util.BuildSettings 2 | import grails.util.Environment 3 | 4 | // See http://logback.qos.ch/manual/groovy.html for details on configuration 5 | appender('STDOUT', ConsoleAppender) { 6 | encoder(PatternLayoutEncoder) { 7 | pattern = "%level %logger - %msg%n" 8 | } 9 | } 10 | 11 | root(ERROR, ['STDOUT']) 12 | 13 | def targetDir = BuildSettings.TARGET_DIR 14 | if (Environment.isDevelopmentMode() && targetDir) { 15 | appender("FULL_STACKTRACE", FileAppender) { 16 | file = "${targetDir}/stacktrace.log" 17 | append = true 18 | encoder(PatternLayoutEncoder) { 19 | pattern = "%level %logger - %msg%n" 20 | } 21 | } 22 | logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) 23 | } 24 | -------------------------------------------------------------------------------- /src/docs/configuration-plugin.adoc: -------------------------------------------------------------------------------- 1 | === Configure Plugin 2 | 3 | |====== 4 | | *Name* | *Default Value* 5 | | grails.plugin.springsecurity.facebook.autoCreate.enabled | true 6 | | grails.plugin.springsecurity.facebook.autoCreate.roles | ['ROLE_USER', 'ROLE_FACEBOOK'] 7 | |====== 8 | 9 | * `autoCreate.enabled` - enable/disabled automatic creation of Application User for a new Facebook user (when FB user first time authenticates) 10 | * `autoCreate.roles` - list of roles to set to a newly created user (if enabled) 11 | 12 | |====== 13 | | *Name* | *Default Value* 14 | | grails.plugin.springsecurity.facebook.host | '' 15 | |====== 16 | 17 | Set a hostname of current app, could be used when user logged out, but FB didn't clear all cookies for domain. Note: it's 18 | host name, not url. Like `example.com` 19 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/DomainsRelation.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | /** 6 | * 7 | * @author Igor Artamonov (http://igorartamonov.com) 8 | * @since 27.12.11 9 | */ 10 | @CompileStatic 11 | enum DomainsRelation { 12 | 13 | SameObject, 14 | JoinedUser 15 | 16 | static DomainsRelation getFrom(x) { 17 | if (!x) { 18 | return JoinedUser 19 | } 20 | if (x instanceof DomainsRelation) { 21 | return (DomainsRelation)x 22 | } 23 | x = x.toString() 24 | DomainsRelation found = DomainsRelation.values().find { DomainsRelation dr -> 25 | dr.name().equalsIgnoreCase((String)x) 26 | } 27 | found ?: JoinedUser 28 | } 29 | } -------------------------------------------------------------------------------- /src/docs/ref/Tags/init.gdoc: -------------------------------------------------------------------------------- 1 | h1. connect 2 | 3 | Add Facebook Javascript SDK initialization code. You could also provide extra initialization JS in the body of 4 | this tag, it will be executed just after Facebook SDK initialization. 5 | 6 | {note} 7 | is useful only for client-side authentication. 8 | {note} 9 | 10 | h2. Attributes: 11 | 12 | * @force@ Force tag to put FB SDK initialization code (even if it's already added) 13 | * @lang@ - locale, for Facebook @all.js@ (like 'en_US', 'ru_RU', etc) 14 | 15 | h2. Example: 16 | ---- 17 | 18 | 21 | FB.Event.subscribe('auth.login', function() { 22 | console.log('Process auth.login...'); 23 | window.location.reload(); 24 | }); 25 | 26 | ---- 27 | -------------------------------------------------------------------------------- /src/docs/configuration.adoc: -------------------------------------------------------------------------------- 1 | == Configuration 2 | 3 | === Basic Configuration 4 | 5 | [NOTE] 6 | ==== 7 | Make sure that you have installed and configured spring-security-core plugin before this step. 8 | ==== 9 | 10 | Most default configuration will look like: 11 | 12 | ---- 13 | grails: 14 | plugin: 15 | springsecurity: 16 | facebook: 17 | domain: 18 | classname: '' 19 | secret: '' 20 | appId: '' 21 | ---- 22 | 23 | When you have valid configuration you can put Facebook Connect button in you GSP: 24 | ---- 25 | 26 | ---- 27 | 28 | You don't need to add anything else. 29 | 30 | include::configuration-fb-app.adoc[] 31 | 32 | include::configuration-domains.adoc[] 33 | 34 | include::configuration-login-btn.adoc[] 35 | 36 | include::configuration-plugin.adoc[] 37 | 38 | include::configuration-types.adoc[] -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthToken.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import org.springframework.security.authentication.AbstractAuthenticationToken 6 | import org.springframework.security.core.GrantedAuthority 7 | 8 | @CompileStatic 9 | class FacebookAuthToken extends AbstractAuthenticationToken { 10 | 11 | private static final long serialVersionUID = 7064510496527L; 12 | 13 | Long uid 14 | FacebookAccessToken accessToken 15 | String code 16 | String redirectUri 17 | 18 | def principal 19 | 20 | Collection authorities = [] as Collection 21 | 22 | FacebookAuthToken() { 23 | super([] as Collection) 24 | } 25 | 26 | def getCredentials() { uid } 27 | 28 | String toString() { 29 | "Principal: $principal, uid: $uid, roles: ${authorities?.collect { GrantedAuthority it -> it?.authority }}" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/docs/configuration-login-btn.adoc: -------------------------------------------------------------------------------- 1 | === Configure Login Button 2 | 3 | ==== Button configuration 4 | 5 | |====== 6 | | *Name* | *Default Value* 7 | | grails.plugin.springsecurity.facebook.taglib.button.text | 'Login with Facebook' 8 | |====== 9 | 10 | ==== Button for Server Side authentication (default) 11 | 12 | Standard `` will be used for button, with following extra configuration options: 13 | 14 | |====== 15 | | *Name* | *Default Value* 16 | | grails.plugin.springsecurity.facebook.taglib.button.img | an url for image distributed with plugin 17 | |====== 18 | 19 | * `img` - url of a default image to use for button 20 | 21 | ==== Button for Client Side authentication 22 | 23 | At this case a HTML button, provided by Facebook JS SDK, will be user. 24 | 25 | |====== 26 | | *Name* | *Default Value* 27 | | grails.plugin.springsecurity.facebook.taglib.language | 'en_US' 28 | |====== 29 | 30 | * `language` - language for Facebook Javascript SDK. You could also pass this option as a `lang` attribute for `:connect` or `:init` tags 31 | -------------------------------------------------------------------------------- /src/docs/configuration-domains.adoc: -------------------------------------------------------------------------------- 1 | === Configure Domains 2 | 3 | |====== 4 | | *Name* | *Default Value* | *Values* 5 | | grails.plugin.springsecurity.facebook.domain.classname | 'FacebookUser' | 6 | | grails.plugin.springsecurity.facebook.domain.appUserConnectionPropertyName | 'user' | 7 | |====== 8 | 9 | * `domain.classname` - name of your domain class, used to store Facebook User details (uid, access_token, etc). 10 | Could be same as configured for Spring Security Core, or a own domain, just for Facebook User details. 11 | 12 | If you're using own domain for Facebook user (a joined to main User) you should 13 | specify `domain.appUserConnectionPropertyName`: it's how your domain class is related 14 | to main (used by Spring Security Core) user domain. It's the name 15 | of the property, usually defined as `static belongsTo = [user: User]` in your Facebook User domain class. 16 | 17 | ==== User creation/initialization 18 | 19 | |====== 20 | | *Name* | *Default Value* 21 | | grails.plugin.springsecurity.facebook.autoCreate.roles | ['ROLE_USER', 'ROLE_FACEBOOK'] 22 | |====== 23 | 24 | List of roles for user created by the plugin. 25 | -------------------------------------------------------------------------------- /src/docs/how-to.adoc: -------------------------------------------------------------------------------- 1 | == How To 2 | 3 | === How To 4 | 5 | ==== How to get user full name and/or email? 6 | 7 | Main goal of the plugin is to make authorization. All other usage of Facebook API should be done by 8 | using additional library, http://www.springsource.org/spring-social[Spring Social] for example. 9 | 10 | First of all: you need 'email' permission on connect `` 11 | 12 | Add Spring Social lib into your classpath, by adding following dependencies into your `build.gradle`: 13 | 14 | ---- 15 | compile 'org.springframework.social:spring-social-facebook:2.0.3.RELEASE' 16 | ---- 17 | 18 | and then you can use Facebook API. For example you can fetch user email and full name on user creation step: 19 | 20 | ---- 21 | def facebook = new FacebookTemplate(token.accessToken.accessToken) 22 | def fbProfile = facebook.userOperations().userProfile 23 | String email = fbProfile.email 24 | String name = fbProfile.name 25 | ---- 26 | 27 | See documentations for Spring Social Facebook: http://docs.spring.io/spring-social-facebook/docs/2.0.3.RELEASE/reference/htmlsingle/#retrieving-a-user-s-profile-data -------------------------------------------------------------------------------- /src/docs/installation.adoc: -------------------------------------------------------------------------------- 1 | == Install plugin 2 | 3 | Basically it's just adding dependency into `build.gradle`: 4 | 5 | ---- 6 | dependencies { 7 | 8 | compile 'org.grails.plugins:spring-security-facebook:0.19.2' 9 | 10 | } 11 | ---- 12 | 13 | Follow Configuration and Basic Usage sections for next steps. 14 | 15 | === Upgrade Notes 16 | 17 | ==== Upgrading from version 0.9 18 | 19 | Since version 0.10 plugin have started to use Server Side authentication by default, instead of 20 | Client Side authentication (based on Facebook JS SDK) that was default implementation for version 0.9 and earlier. 21 | 22 | If you want to continue using Client Side authentication, you should add following configuration into `application.yml`: 23 | 24 | ---- 25 | grails: 26 | plugin: 27 | springsecurity: 28 | facebook: 29 | filter: 30 | type: 31 | - transparent 32 | - cookieDirect 33 | ---- 34 | 35 | ==== Upgrading from version 0.15.x 36 | 37 | Version 0.16 requires a Spring Security Core 2.0, you need to upgrade to this version of core plugin. 38 | 39 | ==== Upgrade from version 0.17.x 40 | 41 | Version 0.18 is based on Grails 3.0 and Spring Security Core 3.0 -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Facebook Authentication plugin for Grails 2 | ========================================= 3 | 4 | Grails plugin for Facebook Authentication, extension to [Grails Spring Security Core plugin](http://www.grails.org/plugin/spring-security-core) 5 | 6 | **PROJECT IS LOOKING FOR A NEW MAINTAINER** 7 | 8 | **Please contract igor@artamonov.ru if you would like to work on the plugin, we'll move it to your repo** 9 | 10 | Requirements 11 | ------------ 12 | 13 | * Grails 3.x 14 | * Spring-Security-Core plugin 3.x 15 | 16 | Examples: 17 | 18 | * Docs - http://splix.github.io/grails-spring-security-facebook/ 19 | * Example App - https://github.com/splix/grails-facebook-authentication-example 20 | 21 | Installation 22 | ------------ 23 | 24 | ``` 25 | dependencies { 26 | compile 'org.grails.plugins:spring-security-facebook:0.19.2' 27 | } 28 | ``` 29 | 30 | If you have any troubles with getting it from main Grails Plugins repository, add following repository: 31 | ``` 32 | repositories { 33 | maven { 34 | url "http://dl.bintray.com/splix/grails-plugins" 35 | } 36 | } 37 | ``` 38 | 39 | Authors 40 | ------- 41 | 42 | [Igor Artamonov](http://igorartamonov.com), [The 6 Hours](http://the6hours.com) 43 | 44 | License 45 | ------- 46 | 47 | Apache 2.0 48 | -------------------------------------------------------------------------------- /gradle/common.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'idea' 2 | apply plugin: 'war' 3 | apply plugin: 'org.grails.grails-gsp' 4 | 5 | def versionTxt = file('version.txt') 6 | version = versionTxt.exists() ? versionTxt.text.trim() : '0.1' 7 | ext { 8 | gradleWrapperVersion = '2.10' 9 | } 10 | 11 | repositories { 12 | mavenLocal() 13 | mavenCentral() 14 | maven { url 'https://repo.grails.org/grails/core' } 15 | } 16 | 17 | dependencyManagement { 18 | imports { 19 | mavenBom "org.grails:grails-bom:$grailsVersion" 20 | } 21 | applyMavenExclusions false 22 | } 23 | 24 | dependencies { 25 | testCompile 'org.grails:grails-plugin-testing' 26 | } 27 | 28 | // deletes everything from the build directory except for test reports 29 | task cleanBuild { 30 | if (!buildDir.exists()) return 31 | 32 | buildDir.eachFile { 33 | if (it.file) { 34 | it.delete() 35 | } 36 | else if (it.name != 'reports' && !it.name.startsWith('geb-reports') && !it.name.startsWith('test-results')) { 37 | it.deleteDir() 38 | } 39 | } 40 | } 41 | 42 | test { 43 | testLogging { 44 | exceptionFormat = 'full' 45 | events 'failed', 'standardOut', 'standardError' 46 | } 47 | 48 | beforeTest { descriptor -> logger.quiet " -- $descriptor" } 49 | } 50 | 51 | task wrapper(type: Wrapper) { 52 | gradleVersion = gradleWrapperVersion 53 | } 54 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthCookieDirectFilter.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import javax.servlet.http.Cookie 6 | import javax.servlet.http.HttpServletRequest 7 | import javax.servlet.http.HttpServletResponse 8 | 9 | import org.springframework.security.core.Authentication 10 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter 11 | 12 | /** 13 | * 14 | * @author Igor Artamonov (http://igorartamonov.com) 15 | * @since 05.07.12 16 | */ 17 | @CompileStatic 18 | class FacebookAuthCookieDirectFilter extends AbstractAuthenticationProcessingFilter { 19 | 20 | FacebookAuthUtils facebookAuthUtils 21 | 22 | FacebookAuthCookieDirectFilter(String defaultFilterProcessesUrl) { 23 | super(defaultFilterProcessesUrl) 24 | } 25 | 26 | @Override 27 | Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { 28 | Cookie cookie = facebookAuthUtils.getAuthCookie(request) 29 | if (!cookie || !cookie.value) { 30 | throw new InvalidCookieException("No cookie") 31 | } 32 | authenticationManager.authenticate facebookAuthUtils.build(cookie.value) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gradle/grailsCentralPublishing.gradle: -------------------------------------------------------------------------------- 1 | publishing.publications { 2 | maven(MavenPublication) { 3 | pom.withXml { 4 | def pomNode = asNode() 5 | pomNode.dependencyManagement.replaceNode {} 6 | 7 | // simply remove dependencies without a version 8 | // version-less dependencies are handled with dependencyManagement 9 | // see https://github.com/spring-gradle-plugins/dependency-management-plugin/issues/8 for more complete solutions 10 | pomNode.dependencies.dependency.findAll { 11 | it.version.text().isEmpty() 12 | }.each { 13 | it.replaceNode {} 14 | } 15 | } 16 | artifactId project.name 17 | from components.java 18 | artifact sourcesJar 19 | artifact javadocJar 20 | artifact source:"${sourceSets.main.output.classesDir}/META-INF/grails-plugin.xml", 21 | classifier:"plugin", 22 | extension:'xml' 23 | } 24 | } 25 | 26 | publishing.repositories { 27 | maven { 28 | credentials { 29 | username System.getenv('GRAILS_CENTRAL_USERNAME') ?: project.properties.get('grailsPluginsUsername') 30 | password System.getenv("GRAILS_CENTRAL_PASSWORD") ?: project.properties.get('grailsPluginsPassword') 31 | } 32 | 33 | if(version.endsWith('-SNAPSHOT')) { 34 | url "https://repo.grails.org/grails/plugins3-snapshots-local" 35 | } 36 | } 37 | } 38 | 39 | task install(dependsOn: project.tasks.withType(PublishToMavenLocal)) -------------------------------------------------------------------------------- /gradle/testapp.gradle: -------------------------------------------------------------------------------- 1 | File pluginDir = file('.').parentFile 2 | File versionTxt 3 | while (true) { 4 | versionTxt = new File(pluginDir, 'version.txt') 5 | if (versionTxt.exists()) { 6 | break 7 | } 8 | pluginDir = pluginDir.parentFile 9 | } 10 | 11 | apply plugin: 'org.grails.grails-web' 12 | 13 | apply from: new File(pluginDir, 'gradle/common.gradle').path 14 | 15 | project.ext.pluginVersion = versionTxt.text.trim() 16 | project.ext.pluginName = pluginDir.name - 'grails-' 17 | 18 | dependencies { 19 | compile "org.grails.plugins:$pluginName:$pluginVersion" 20 | compile 'com.h2database:h2:1.4.190' 21 | compile 'org.grails:grails-dependencies' 22 | compile 'org.springframework.boot:spring-boot-autoconfigure' 23 | compile 'org.springframework.boot:spring-boot-starter-logging' 24 | compile 'org.springframework.boot:spring-boot-starter-tomcat' 25 | console 'org.grails:grails-console' 26 | runtime 'org.grails.plugins:asset-pipeline' 27 | testCompile 'org.gebish:geb-core:0.12.2' 28 | testCompile 'org.grails.plugins:geb' 29 | 30 | String seleniumVersion = '2.48.2' 31 | // testCompile 'com.github.detro:phantomjsdriver:1.2.0' 32 | testCompile 'com.codeborne:phantomjsdriver:1.2.1' // TODO switch back to com.github.detro:phantomjsdriver when this 33 | // issue is resolved: https://github.com/detro/ghostdriver/issues/397 34 | 35 | testCompile "org.seleniumhq.selenium:selenium-support:$seleniumVersion" 36 | ['chrome', 'firefox'].each { String name -> 37 | testCompile "org.seleniumhq.selenium:selenium-${name}-driver:$seleniumVersion" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /grails-app/conf/DefaultFacebookSecurityConfig.groovy: -------------------------------------------------------------------------------- 1 | security { 2 | 3 | facebook { 4 | 5 | appId = "Invalid" 6 | secret = 'Invalid' 7 | apiKey = 'Invalid' 8 | 9 | domain { 10 | classname = 'FacebookUser' 11 | appUserConnectionPropertyName = "user" 12 | } 13 | 14 | useAjax = true 15 | autoCheck = true 16 | 17 | jsconf = "fbSecurity" 18 | 19 | //see http://developers.facebook.com/docs/authentication/permissions/ 20 | permissions = ["email"] 21 | 22 | taglib { 23 | language = "en_US" 24 | button { 25 | text = "Login with Facebook" 26 | } 27 | initfb = true 28 | } 29 | 30 | autoCreate { 31 | enabled = true 32 | roles = ['ROLE_USER', 'ROLE_FACEBOOK'] 33 | } 34 | 35 | filter { 36 | json { 37 | processUrl = "/j_spring_security_facebook_json" 38 | type = 'json' // or 'jsonp' 39 | methods = ['POST'] 40 | } 41 | redirect { 42 | redirectFromUrl = "/j_spring_security_facebook_redirect" 43 | } 44 | processUrl = "/j_spring_security_facebook_check" 45 | type = 'redirect' //transparent, cookieDirect, redirect or json 46 | position = 720 //see SecurityFilterPosition 47 | forceLoginParameter = 'j_spring_facebook_force' 48 | } 49 | 50 | beans { 51 | //successHandler = 52 | //failureHandler = 53 | //redirectSuccessHandler = 54 | //redirectFailureHandler = 55 | } 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/docs/common-issues.adoc: -------------------------------------------------------------------------------- 1 | == Common Issues 2 | 3 | === Debug 4 | 5 | ==== Enable logging 6 | 7 | If you have troubles with plugin, please enabled logging, so you can see what's happening. Add to `logback.groovy` following: 8 | ---- 9 | logger("com.the6hours", DEBUG, ["CONSOLE"]) 10 | ---- 11 | 12 | === Issues 13 | 14 | ==== Client side authentication don't work on dev server 15 | 16 | Make sure that you're using a real domain name for your application. Not a `localhost`, because Facebook can't setup 17 | cookie for localhost, and avoid +.local+ domains as well. 18 | 19 | You can make a fake domain like +myapp.dev+, by putting into `/etc/hosts` the following line: 20 | ---- 21 | 127.0.0.1 myapp.dev 22 | ---- 23 | If you already have line starting with `127.0.0.1`, just add your `myapp.dev` at the end of the line. 24 | 25 | See more details about hosts file, and location of the file for different operation systems see: http://en.wikipedia.org/wiki/Hosts_(file) 26 | 27 | After that, you should configure your Grails app to use this domain, by adding following line into `conf/application.yml`: 28 | ---- 29 | grails: 30 | serverURL: "http://myapp.dev:8080/${appName}" 31 | ---- 32 | 33 | Of course, you need to use this domain only for development, so put this configuration into `development` 34 | environment config: 35 | ---- 36 | environments: 37 | development: 38 | grails: 39 | serverURL: "http://myapp.dev:8080/${appName}" 40 | ---- 41 | 42 | ==== Logout doesn't work for Transparent filter 43 | 44 | "transparent" filter always authorize request that contains FB cookie. If you need to log out current user when using 45 | this type of filter, you need to log out user on client side, call `FB.logout()` (will logout user from Facebook 46 | as well) and reload the page. -------------------------------------------------------------------------------- /src/docs/usage-client-side.adoc: -------------------------------------------------------------------------------- 1 | === Client Side Authentication 2 | 3 | Based on https://developers.facebook.com/docs/javascript[Facebook Javascript SDK] authorization. Useful 4 | when you need to use FB Javascript SDK on client side. 5 | 6 | And there are two ways: 7 | 8 | * try authenticate user on each request, to any page - it's `transparent` filter 9 | * authenticate only when user redirected to specified page, like a standard username/password authentication - it's `cookieDirect` filter 10 | 11 | ==== Transparent filter 12 | 13 | If you're using first way (`transparent` filter), your user will be automatically authenticated whenever he has 14 | Facebook cookie. Btw, don't forget that you should *reload* current page after you have successfully authenticated user 15 | on client side. Like: 16 | 17 | ---- 18 | 19 | FB.Event.subscribe('auth.login', function() { 20 | window.location.reload(); 21 | }); 22 | 23 | ---- 24 | 25 | To logout user, simply call `FB.logout()` (using Javascript) on client side. 26 | 27 | ==== CookieDirect filter 28 | 29 | If you're using second way (`cookieDirect` filter), you could configure URL that will be used for authentication at `application.yml`: 30 | 31 | ---- 32 | grails: 33 | plugin: 34 | springsecurity: 35 | facebook: 36 | filter: 37 | processUrl: '/j_spring_security_facebook_check' # it's default value 38 | ---- 39 | 40 | And after authorization redirect user to `/j_spring_security_facebook_check`, like: 41 | 42 | ---- 43 | 44 | FB.Event.subscribe('auth.login', function() { 45 | window.location.href = '/j_spring_security_facebook_check' 46 | }); 47 | 48 | 49 | 50 | $('#fbloginbutton').click(function() { 51 | FB.login(); 52 | }); 53 | 54 | ---- -------------------------------------------------------------------------------- /src/docs/usage-json.adoc: -------------------------------------------------------------------------------- 1 | === Json Authentication 2 | 3 | Filter 'FacebookAuthJsonFilter' accepts Facebook Access Token or Signed Request as parameter, and responds 4 | with JSON to authorization requests. It's useful if you an external client for your Grails application, it 5 | could be Android or iOS application, or Desktop application, or just AJAX client. 6 | 7 | [NOTE] 8 | ==== 9 | JSON filter just returns an object with user details, nothing else. For authentication of other requests, 10 | you still have to use different filter. If you have a RESTful client, take a look at 11 | http://grails.org/plugin/spring-security-oauth2-provider[spring-security-oauth2-provider plugin] 12 | ==== 13 | 14 | How it works: 15 | 16 | ---- 17 | > GET /j_spring_security_facebook_json?access_token= 18 | ---- 19 | 20 | For successful authorization you'll get: 21 | ---- 22 | < HTTP/1.1 200 OK 23 | < Content-Type: application/json;charset=UTF-8 24 | < 25 | { 26 | "authenticated": true, 27 | "uid": 12345612345, # Facebook User Id 28 | "roles":[ 29 | "ROLE_FACEBOOK", 30 | "ROLE_USER" 31 | ], 32 | "username": "facebook_12345612345", # Grails Application User Id/Username 33 | "enabled": true # Grails Application User status 34 | } 35 | ---- 36 | 37 | For unsuccessful: 38 | ---- 39 | < HTTP/1.1 401 Unauthorized 40 | < Content-Type: application/json;charset=UTF-8 41 | < 42 | { 43 | "authenticated": false, 44 | "message": "Expired token" # Authentication Failure reason 45 | } 46 | ---- 47 | 48 | ==== How to extend JSON response 49 | 50 | The plugin going to call `Map onJsonSuccess(Map input, FacebookAuthToken token)` 51 | or `Map onJsonFailure(Map input, AuthenticationException exception)` methods 52 | of FacebookAuthService (if exists). 53 | 54 | There you can update `input` data with any other values, introduce new fields/keys, or even return your 55 | own structure. This structure will be transformed to JSON and sent to client. 56 | -------------------------------------------------------------------------------- /src/docs/ref/Tags/connect.gdoc: -------------------------------------------------------------------------------- 1 | h1. connect 2 | 3 | Puts Facebook Connect button. If Facebook Javascript SDK initialization code wasn't added into current page, 4 | then calling of this tag will call @:init@ automatically. 5 | 6 | Optional attributes: 7 | * @type@ - @'server'@ or @'client'@, type of authentication to use. By default it uses server-side authentication (if such authentication type is enabled) 8 | 9 | Optional attributes for @server@ button (server-side authentication): 10 | * @img@ - url to image for connect button (for server-side authentication only) 11 | * @img-*@ - a attribute for tag, like @img-id@ or @img-style@ 12 | * @startUrl@ - a local url that will prepare valid redirect to Facebook API, instead of default one provided by the plugin. At most cases you don't need to specify this, do it only if you know what you're doing and default implementation is not enough for your case. 13 | 14 | Optional attributes for @client@ button (client-side authentication): 15 | * @permission@ - list of [Facebook Permission|https://developers.facebook.com/docs/reference/login/] as a String or List 16 | * @text@ - text to display on button, *Login with Facebook* by default 17 | * @skipInit@ - force to skip Facebook Javascript SDK initialization. By default this tag calls @@, if it's not executed yet. 18 | * @lang@ - locale, for Facebook @all.js@ (like 'en_US', 'ru_RU', etc) 19 | 20 | You can get official FB button image at [https://developers.facebook.com/docs/facebook-login/checklist/|https://developers.facebook.com/docs/facebook-login/checklist/#brandedlogin] 21 | 22 | h2. Examples: 23 | 24 | Default: 25 | ---- 26 | 27 | ---- 28 | 29 | Use your image for connect button: 30 | ---- 31 | 32 | ---- 33 | 34 | Ask for email: 35 | ---- 36 | 37 | ---- 38 | 39 | -------------------------------------------------------------------------------- /src/docs/usage-server-side.adoc: -------------------------------------------------------------------------------- 1 | === Service Side 2 | 3 | It's the `FacebookAuthRedirectFilter`, enabled by default. 4 | 5 | It's preferred and a standard https://developers.facebook.com/docs/howtos/login/server-side-login/[Login for Server-side Apps]. 6 | After clicking on 'connect button' user gets redirected to special Facebook page, for authentication, and then 7 | redirected back to your app. 8 | 9 | [NOTE] 10 | ==== 11 | User going to see Facebook Authentication screen only at the first time. Next time user will be redirected 12 | back from Facebook to your application immediately. 13 | ==== 14 | 15 | ==== How to process failed login 16 | 17 | When user declines Facebook Authentication (click Cancel, for example), you'll '401 Authentication Failed' by default. 18 | It's default configuration of Spring Security failure handler, but for most cases it's not what you really want. 19 | 20 | To handle this situation you have to create your own Failure Handler, a bean implementing 21 | `org.springframework.security.web.authentication.AuthenticationFailureHandler`. If you just need to 22 | show a page (a GSP view), you can use standard `SimpleUrlAuthenticationFailureHandler`, that could redirect 23 | failed authentication to specified URL. 24 | 25 | For example you can create bean at `resources.groovy`: 26 | 27 | ---- 28 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler 29 | 30 | // Place your Spring DSL code here 31 | beans = { 32 | 33 | redirectFailureHandlerExample(SimpleUrlAuthenticationFailureHandler) { 34 | defaultFailureUrl = '/failed' //redirect to this URL when authentication fails 35 | } 36 | 37 | } 38 | ---- 39 | 40 | and setup this bean for 'redirect' filter at `application.yml`: 41 | 42 | ---- 43 | grails: 44 | plugin: 45 | springsecurity: 46 | facebook: 47 | filter: 48 | redirect: 49 | failureHandler: redirectFailureHandlerExample 50 | ---- 51 | 52 | Same way for configuring Success Handler. 53 | -------------------------------------------------------------------------------- /src/test/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthTokenSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import grails.test.mixin.TestMixin 4 | import grails.test.mixin.support.GrailsUnitTestMixin 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority 6 | import spock.lang.Specification 7 | 8 | import com.the6hours.grails.springsecurity.facebook.FacebookAuthToken 9 | 10 | /** 11 | * See the API for {@link grails.test.mixin.support.GrailsUnitTestMixin} for usage instructions 12 | */ 13 | @TestMixin(GrailsUnitTestMixin) 14 | class FacebookAuthTokenSpec extends Specification { 15 | 16 | def setup() { 17 | } 18 | 19 | def cleanup() { 20 | } 21 | 22 | void "authorities should not be null after construction"() { 23 | when: 24 | FacebookAuthToken token = new FacebookAuthToken() 25 | 26 | then: 27 | token.authorities != null 28 | } 29 | 30 | void "toString works"() { 31 | when: 32 | FacebookAuthToken token = new FacebookAuthToken() 33 | token.authorities = [new SimpleGrantedAuthority('ROLE_USER')] 34 | token.principal = 'test' 35 | token.uid = 123456 36 | def act = token.toString() 37 | 38 | then: 39 | act == 'Principal: test, uid: 123456, roles: [ROLE_USER]' 40 | } 41 | 42 | void "toString works with null authorities"() { 43 | when: 44 | FacebookAuthToken token = new FacebookAuthToken() 45 | token.authorities = null 46 | token.principal = 'test' 47 | token.uid = 123456 48 | def act = token.toString() 49 | 50 | then: 51 | act == 'Principal: test, uid: 123456, roles: null' 52 | } 53 | 54 | void "toString works with invalid authorities"() { 55 | when: 56 | FacebookAuthToken token = new FacebookAuthToken() 57 | token.authorities = [new SimpleGrantedAuthority('ROLE_USER'), null] 58 | token.principal = 'test' 59 | token.uid = 123456 60 | def act = token.toString() 61 | 62 | then: 63 | act == 'Principal: test, uid: 123456, roles: [ROLE_USER, null]' 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/docs/usage-basic.adoc: -------------------------------------------------------------------------------- 1 | === Example app 2 | 3 | You can take a look at https://github.com/splix/grails-facebook-authentication-example[Example Application], it's very 4 | basic app, that have only one page, with 'Facebook Connect' button. Just clone it, put your FB App credentials, and play with it. 5 | 6 | ==== Add Facebook Authentication into your existing application 7 | 8 | ===== Initial plugin config 9 | 10 | ====== Domain Class 11 | 12 | Create domain class for your facebook user: 13 | ---- 14 | class FacebookUser { 15 | Long uid 16 | String accessToken 17 | Date accessTokenExpires 18 | 19 | static belongsTo = [user: User] //connected to main Spring Security domain 20 | 21 | static constraints = { 22 | uid unique: true 23 | } 24 | } 25 | ---- 26 | 27 | At `conf/application.yml` setup full name (including package name, if used) of just created Facebook user domain, like: 28 | 29 | ---- 30 | grails: 31 | plugin: 32 | springsecurity: 33 | facebook: 34 | domain: 35 | classname: 'FacebookUser' 36 | ---- 37 | 38 | ====== Add FB App credentials 39 | 40 | You should create a Facebook App and copy App ID and Secret: 41 | 42 | !create_app.png! 43 | 44 | into `conf/application.yml`: 45 | ---- 46 | grails: 47 | plugin: 48 | springsecurity: 49 | facebook: 50 | appId: 12345678900000 51 | secret: 76c2279743c99da3715e3d00f29a1234 52 | ---- 53 | 54 | PS it's just example, you should use your own `appId` and `secret`. 55 | 56 | ====== Add Facebook Connect button 57 | 58 | There is special taglib (` 63 | 64 | 65 | 66 | Welcome ! (Logout) 67 | 68 | ---- 69 | 70 | ===== Run 71 | 72 | That's it! Run your application, and test that everything is working. 73 | 74 | ---- 75 | grails run-app 76 | ---- -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthJsonFilter.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import javax.servlet.http.HttpServletRequest 6 | import javax.servlet.http.HttpServletResponse 7 | 8 | import org.apache.commons.lang.StringUtils 9 | import org.slf4j.Logger 10 | import org.slf4j.LoggerFactory 11 | import org.springframework.security.core.Authentication 12 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter 13 | 14 | @CompileStatic 15 | class FacebookAuthJsonFilter extends AbstractAuthenticationProcessingFilter { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(this) 18 | 19 | FacebookAuthUtils facebookAuthUtils 20 | 21 | List methods = ['POST'] 22 | 23 | FacebookAuthJsonFilter(String url) { 24 | super(url) 25 | } 26 | 27 | Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { 28 | String method = request.method?.toUpperCase() ?: 'UNKNOWN' 29 | if (!methods.contains(method)) { 30 | log.error("Request method: $method, allowed only $methods") 31 | throw new InvalidRequestException("$method is not accepted") 32 | } 33 | 34 | FacebookAuthToken token 35 | 36 | if (StringUtils.isNotEmpty(request.getParameter('access_token'))) { 37 | String accessTokenValue = request.getParameter('access_token') 38 | FacebookAccessToken accessToken = facebookAuthUtils.refreshAccessToken(accessTokenValue) 39 | if (!accessToken) { 40 | throw new InvalidRequestException("Invalid access_token value (or expired)") 41 | } 42 | 43 | token = new FacebookAuthToken(accessToken: accessToken, authenticated: true) 44 | return authenticationManager.authenticate(token) 45 | } 46 | 47 | if (StringUtils.isNotEmpty(request.getParameter('signed_request'))) { 48 | token = facebookAuthUtils.build(request.getParameter('signed_request')) 49 | } else if (StringUtils.isNotEmpty(request.getParameter('signedRequest'))) { //TODO remove. for backward compatibility only 50 | token = facebookAuthUtils.build(request.getParameter('signedRequest')) 51 | } 52 | if (!token) { 53 | throw new InvalidRequestException("Client didn't provide any details for authorization") 54 | } 55 | 56 | authenticationManager.authenticate token 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthDao.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import org.springframework.security.core.GrantedAuthority 4 | 5 | /** 6 | * @param The Facebook user domain class 7 | * @param The application user domain class 8 | */ 9 | interface FacebookAuthDao { 10 | 11 | /** 12 | * Tries to load app user for Facebook user 13 | * @param uid UID of Facebook user 14 | * @return existing user, or null if there is no user for specified uid 15 | */ 16 | F findUser(long uid) 17 | 18 | /** 19 | * Called when logged in facebook user doesn't exists in current database 20 | * @param token information about current authentication 21 | * @return just created user 22 | */ 23 | F create(FacebookAuthToken token) 24 | 25 | /** 26 | * Returns `principal` that will be stored into Security Context. It's good if it 27 | * implements {@link org.springframework.security.core.userdetails.UserDetails UserDetails} or 28 | * {@link java.security.Principal Principal}. 29 | * 30 | * Btw, it's ok to return same object here. 31 | * 32 | * @param user current app user (main spring security core domain instance) 33 | * @return user to put into Security Context 34 | */ 35 | Object getPrincipal(A user) 36 | 37 | /** 38 | * Return main (spring security user domain) for given facebook user. If it's same domain, just return 39 | * passed argument. 40 | * 41 | * @param user instance of facebook domain 42 | * @return instance of spring security domain 43 | */ 44 | A getAppUser(F user) 45 | 46 | /** 47 | * Roles for current user 48 | * 49 | * @param user current user 50 | * @return roles for user 51 | */ 52 | Collection getRoles(A user) 53 | 54 | /** 55 | * 56 | * @param user target user 57 | * @return false when user have invalid token, or don't have token 58 | */ 59 | Boolean hasValidToken(F user) 60 | 61 | /** 62 | * Setup a new Facebook Access Token if needed. Could be called with existing token, so 63 | * implementation should check this case. 64 | * 65 | * @param user target user 66 | * @param token valid access token 67 | */ 68 | void updateToken(F user, FacebookAuthToken token) 69 | 70 | /** 71 | * 72 | * @param user target user 73 | * @return current access_token, or null if not exists 74 | */ 75 | String getAccessToken(F user) 76 | } 77 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/templates/FacebookAuthDaoImpl.groovy.template: -------------------------------------------------------------------------------- 1 | ${packageDeclaration} 2 | 3 | import com.the6hours.grails.springsecurity.facebook.FacebookAuthDao 4 | import com.the6hours.grails.springsecurity.facebook.FacebookAuthToken 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory 7 | import org.springframework.security.core.GrantedAuthority 8 | import ${domainClassFullName} 9 | 10 | /** 11 | * Autogenerated implementation of FacebookAuthDao 12 | * 13 | */ 14 | class ${daoClassName} implements FacebookAuthDao<${domainClassName}> { 15 | 16 | private static final Logger logger = LoggerFactory.getLogger(this) 17 | 18 | /** 19 | * Tries to load app user for Facebook user 20 | * 21 | * @param uid UID of Facebook user 22 | * @return existing user, or null if there is no user for specified uid 23 | */ 24 | ${domainClassName} findUser(long uid) { 25 | //must be fine for most cases 26 | ${domainClassName}.findByUid uid 27 | } 28 | 29 | /** 30 | * Called when logged in facebook user doesn't exists in current database 31 | * 32 | * @param token information about current authnetication 33 | * @return just created user 34 | */ 35 | ${domainClassName} create(FacebookAuthToken token) { 36 | ${domainClassName} user = new ${domainClassName}( 37 | accessToken: token.accessToken, 38 | uid: token.uid 39 | ) 40 | // TODO set relations with your own domains (user,roles) 41 | // TODO fill with extra data from Facebook API 42 | ${domainClassName}.withTransaction { 43 | user.save() 44 | } 45 | user 46 | } 47 | 48 | /** 49 | * Called when facebook auth token has been changed 50 | * 51 | * @param user updates used details 52 | */ 53 | void update(${domainClassName} user) { 54 | // TODO change regarding your domains structure 55 | ${domainClassName}.withTransaction { 56 | user.save() 57 | } 58 | } 59 | 60 | /** 61 | * Returns `principal` that will be stored into Security Context. It's good if it 62 | * implements {@link org.springframework.security.core.userdetails.UserDetails UserDetails} or 63 | * {@link java.security.Principal Principal}. 64 | * 65 | * At most cases it's just current user, passed as parameter 66 | * 67 | * @param user current user 68 | * @return user to put into Security Context 69 | */ 70 | def getPrincipal(${domainClassName} user) { 71 | // TODO change regarding your domains structure 72 | user 73 | } 74 | 75 | /** 76 | * Roles for current user 77 | * 78 | * @param user current user 79 | * @return roles for user 80 | */ 81 | Collection getRoles(${domainClassName} user) { 82 | // TODO change regarding your domains structure 83 | user.roles 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthProviderSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import org.springframework.security.core.userdetails.UsernameNotFoundException 4 | import spock.lang.Specification 5 | 6 | /** 7 | * 8 | * Created at 20.04.13 9 | * @author Igor Artamonov (http://igorartamonov.com) 10 | */ 11 | class FacebookAuthProviderSpec extends Specification { 12 | 13 | def "Return existing user on second sign-in"() { 14 | setup: 15 | FacebookAuthDao dao = Mock(FacebookAuthDao) 16 | FacebookAuthUtils utils = Mock(FacebookAuthUtils) 17 | FacebookAuthProvider provider = new FacebookAuthProvider( 18 | facebookAuthDao: dao, 19 | facebookAuthUtils: utils 20 | ) 21 | FacebookAuthToken token = new FacebookAuthToken( 22 | uid: 1 23 | ) 24 | TestFacebookUser user = new TestFacebookUser( 25 | uid: 1 26 | ) 27 | TestAppUser appUser = new TestAppUser() 28 | when: 29 | provider.authenticate(token) 30 | then: 31 | 1 * dao.findUser(1) >> user 32 | 1 * dao.getAppUser(user) >> appUser 33 | 1 * dao.getPrincipal(appUser) >> "hello!" 34 | 1 * dao.getRoles(_) >> [] 35 | 1 * utils.getAccessToken(_, _) >> new FacebookAccessToken( 36 | accessToken: 'test', 37 | expireAt: new Date() 38 | ) 39 | } 40 | 41 | def "Create a new user"() { 42 | setup: 43 | FacebookAuthDao dao = Mock(FacebookAuthDao) 44 | FacebookAuthUtils utils = Mock(FacebookAuthUtils) 45 | FacebookAuthProvider provider = new FacebookAuthProvider( 46 | facebookAuthDao: dao, 47 | facebookAuthUtils: utils 48 | ) 49 | FacebookAuthToken token = new FacebookAuthToken( 50 | uid: 1 51 | ) 52 | TestFacebookUser user = new TestFacebookUser( 53 | uid: 1 54 | ) 55 | TestAppUser appUser = new TestAppUser() 56 | FacebookAccessToken accessToken = new FacebookAccessToken( 57 | accessToken: 'test', 58 | expireAt: new Date() 59 | ) 60 | when: 61 | provider.authenticate(token) 62 | then: 63 | 1 * dao.findUser(1) >> null 64 | 1 * utils.getAccessToken(_, _) >> accessToken //load token before creation 65 | 1 * dao.create(token) >> user //create user 66 | 1 * dao.hasValidToken(user) >> true 67 | 1 * dao.getAppUser(user) >> appUser 68 | 1 * dao.getPrincipal(appUser) >> "hello!" 69 | 1 * dao.getRoles(_) >> [] 70 | } 71 | 72 | def "Don't create a new user when disabled"() { 73 | setup: 74 | FacebookAuthDao dao = Mock(FacebookAuthDao) 75 | FacebookAuthUtils utils = Mock(FacebookAuthUtils) 76 | FacebookAuthProvider provider = new FacebookAuthProvider( 77 | facebookAuthDao: dao, 78 | facebookAuthUtils: utils, 79 | createNew: false 80 | ) 81 | FacebookAuthToken token = new FacebookAuthToken( 82 | uid: 1 83 | ) 84 | when: 85 | provider.authenticate(token) 86 | then: 87 | 1 * dao.findUser(1) >> null 88 | thrown(UsernameNotFoundException) 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/docs/configuration-types.adoc: -------------------------------------------------------------------------------- 1 | === Configure Authentication Types 2 | 3 | |====== 4 | | *Name* | *Default Value* 5 | | grails.plugin.springsecurity.facebook.filter.processUrl | '/j_spring_security_facebook_check' 6 | | grails.plugin.springsecurity.facebook.filter.type | 'redirect' 7 | |====== 8 | 9 | * `type` - type of authentication, can be `transparent`, `cookieDirect`, `redirect` or `json`. 10 | 11 | You can specify list of filters as a list `['redirect', 'json']` or comma-separated string: 12 | ---- 13 | grails.plugin.springsecurity.facebook.filter.type='redirect,json' 14 | ---- 15 | 16 | ==== Configuration for REDIRECT filter 17 | 18 | |====== 19 | | *Name* | *Default Value* 20 | | grails.plugin.springsecurity.facebook.filter.redirect.processUrl | not set 21 | | grails.plugin.springsecurity.facebook.filter.redirect.redirectFromUrl | '/j_spring_security_facebook_redirect' 22 | | grails.plugin.springsecurity.facebook.filter.redirect.failureHandler | not set 23 | | grails.plugin.springsecurity.facebook.filter.redirect.successHandler | not set 24 | |====== 25 | 26 | * `redirectFromUrl` - it's the url that will redirect user to special Facebook Authentication URL. You can put link to/redirect user to `redirectFromUrl` when you want to use Facebook authentication. This url is used by default `` button. 27 | * if `filter.redirect.processUrl` is not then, then default `filter.processUrl` will be used 28 | * `failureHandler` - a name of the bean that implements [AuthenticationFailureHandler|http://static.springsource.org/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/web/authentication/AuthenticationFailureHandler.html] 29 | * `successHandler` - a name of the bean that implements [AuthenticationSuccessHandler|http://static.springsource.org/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/web/authentication/AuthenticationSuccessHandler.html] 30 | 31 | ==== Configuration for TRANSPARENT filter 32 | 33 | NA 34 | 35 | ==== Configuration for COOKIEDIRECT filter 36 | 37 | |====== 38 | | *Name* | *Default Value* 39 | | grails.plugin.springsecurity.facebook.filter.cookieDirect.processUrl | not set 40 | | grails.plugin.springsecurity.facebook.filter.cookieDirect.failureHandler | not set 41 | | grails.plugin.springsecurity.facebook.filter.cookieDirect.successHandler | not set 42 | |====== 43 | 44 | * if `filter.cookieDirect.processUrl` is not set, then default `filter.processUrl` will be used 45 | * `failureHandler` - a name of the bean that implements [AuthenticationFailureHandler|http://static.springsource.org/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/web/authentication/AuthenticationFailureHandler.html] 46 | * `successHandler` - a name of the bean that implements [AuthenticationSuccessHandler|http://static.springsource.org/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/web/authentication/AuthenticationSuccessHandler.html] 47 | 48 | ==== Configuration for JSON filter 49 | 50 | |====== 51 | | *Name* | *Default Value* 52 | | grails.plugin.springsecurity.facebook.filter.json.processUrl | '/j_spring_security_facebook_json' 53 | | grails.plugin.springsecurity.facebook.filter.json.type | 'json' 54 | | grails.plugin.springsecurity.facebook.filter.json.methods | ['POST'] 55 | |====== 56 | 57 | * `type` could be `json` (default) or `jsonp` 58 | * `methods` - allowed HTTP methods. Notice that it's used only for JSON, for JSONP it will be forced to 'GET' 59 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthCookieLogoutHandler.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import grails.plugin.springsecurity.SpringSecurityUtils 4 | import groovy.transform.CompileStatic 5 | 6 | import java.util.regex.Matcher 7 | 8 | import javax.servlet.http.Cookie 9 | import javax.servlet.http.HttpServletRequest 10 | import javax.servlet.http.HttpServletResponse 11 | 12 | import org.slf4j.Logger 13 | import org.slf4j.LoggerFactory 14 | import org.springframework.security.core.Authentication 15 | import org.springframework.security.web.authentication.logout.LogoutHandler 16 | 17 | /** 18 | * 19 | * @author Igor Artamonov (http://igorartamonov.com) 20 | * @since 04.11.11 21 | */ 22 | @CompileStatic 23 | class FacebookAuthCookieLogoutHandler implements LogoutHandler { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(this) 26 | 27 | FacebookAuthDao facebookAuthDao 28 | FacebookAuthUtils facebookAuthUtils 29 | boolean cleanupToken = true 30 | 31 | void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) { 32 | String baseDomain = null 33 | String expectedCookieName = ['fbsr', facebookAuthUtils.applicationId].join('_') 34 | 35 | Collection cookies = request.cookies?.findAll { Cookie c -> 36 | c.name == expectedCookieName 37 | } 38 | 39 | if (!cookies) { 40 | logger.debug("No FB cookies") 41 | return 42 | } 43 | 44 | baseDomain = cookies.find { Cookie c -> 45 | c.name == expectedCookieName && c.value ==~ /base_domain=.+/ 46 | }?.value?.split('=')?.last() 47 | 48 | if (!baseDomain) { 49 | //Facebook uses invalid cookie format, so sometimes we need to parse it manually 50 | String rawCookie = request.getHeader('Cookie') 51 | logger.debug("raw cookie: $rawCookie") 52 | if (rawCookie) { 53 | Matcher m = rawCookie =~ /$expectedCookieName=base_domain=(.+?);/ 54 | if (m.find()) { 55 | baseDomain = m.group(1) 56 | } 57 | } 58 | } 59 | 60 | if (!baseDomain) { 61 | ConfigObject conf = (ConfigObject)SpringSecurityUtils.securityConfig.facebook 62 | if (conf.containsKey('host')) { 63 | baseDomain = conf.host 64 | } else { 65 | logger.warn("Can't find base domain for Facebook cookie. Please configure app host name as grails.plugin.springsecurity.facebook.host") 66 | return 67 | } 68 | } 69 | 70 | cookies.each { Cookie cookie -> 71 | cookie.maxAge = 0 72 | cookie.path = '/' 73 | if (baseDomain) { 74 | cookie.domain = baseDomain 75 | } 76 | response.addCookie cookie 77 | } 78 | 79 | if (cleanupToken && (auth instanceof FacebookAuthToken)) { 80 | cleanupToken(auth) 81 | } 82 | } 83 | 84 | void cleanupToken(FacebookAuthToken authentication) { 85 | if (!facebookAuthDao) { 86 | logger.error("No FacebookAuthDao") 87 | return 88 | } 89 | try { 90 | def user = facebookAuthDao.findUser(authentication.uid) 91 | authentication.accessToken = null 92 | facebookAuthDao.updateToken(user, authentication) 93 | } 94 | catch (Throwable t) { 95 | logger.error("Can't remove existing token", t) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/docs/usage-filters.adoc: -------------------------------------------------------------------------------- 1 | === Filters 2 | 3 | ==== How it works 4 | 5 | Plugin is based on Spring Security and uses web filters for authorization, for more details see 6 | http://static.springsource.org/spring-security/site/docs/3.0.x/reference/security-filter-chain.html[Spring Security docs] 7 | 8 | ===== Available filters 9 | 10 | There are 4 types of filter: 11 | 12 | * FacebookAuthRedirectFilter - server-side authorization (used by default) 13 | * FacebookAuthCookieTransparentFilter - automatic client-side authorization 14 | * FacebookAuthCookieDirectFilter - manual client-side authorization 15 | * FacebookAuthJsonFilter - for external clients (like Android/iOS app) 16 | 17 | ===== Server-Side authentication (FacebookAuthRedirectFilter) 18 | 19 | It's a standard https://developers.facebook.com/docs/howtos/login/server-side-login/[Login for Server-side Apps]. 20 | After clicking on 'connect button' user gets redirected to special Facebook page, for authentication, and then 21 | redirected back to your app. 22 | 23 | ==== Client-Side authentication 24 | 25 | ===== Transparent cookie based authorization (FacebookAuthCookieTransparentFilter) 26 | 27 | Based on https://developers.facebook.com/docs/reference/javascript/[Facebook Javascript SDK] authorization. On 28 | client side it makes Facebook authorization and put Facebook Cookie (it's made by Facebook Javascript, 29 | you don't need anything special) 30 | 31 | After successful authorization on client side, the browser should reload current page. Or open any other page. 32 | 33 | This filter will *process each request*, and if it sees valid Facebook cookie, it makes authorization for 34 | current user. If it's a new user, it creates a new one for application, with provided Facebook credentials. 35 | 36 | [NOTE] 37 | ==== 38 | It's per-request authorization. That means that this filter will try to authorize user on each page request. 39 | ==== 40 | 41 | ===== Manual cookie based authentication (FacebookAuthCookieDirectFilter) 42 | 43 | Based on https://developers.facebook.com/docs/reference/javascript/[Facebook Javascript SDK] authorization. On 44 | client side it makes Facebook authorization and put Facebook Cookie (it's made by Facebook Javascript, 45 | you don't need anything special) 46 | 47 | Same as FacebookAuthCookieTransparentFilter, it parse Facebook cookie, but only for specified url. Like 48 | username/password filter from spring-security-core or similar. After successful authorization it 49 | can redirect user to specified url. 50 | 51 | ===== JSON or Android/iOS/desktop authorization (FacebookAuthJsonFilter) 52 | 53 | Client should send Access Token or Signed Request as parameter, and will get JSON response with user details. 54 | 55 | See [filter docs|guide:3.5 Json Authentication] 56 | 57 | === Filter Type configuration 58 | 59 | You can use config parameter `grails.plugin.springsecurity.facebook.filter.type` to configure which filters 60 | you want to use in your application. 61 | 62 | ---- 63 | It's not a Spring Security configuration, not a configuration for Spring filters. Just a 64 | extra configuration, that used only by this plugin. 65 | ---- 66 | 67 | By default it uses only one 'redirect' filter: 68 | ---- 69 | grails: 70 | plugin: 71 | springsecurity: 72 | facebook: 73 | filter: 74 | type: redirect 75 | ---- 76 | 77 | You can use more that one filter at same time: 78 | ---- 79 | grails: 80 | plugin: 81 | springsecurity: 82 | facebook: 83 | filter: 84 | type: 85 | - transparent 86 | - cookieDirect 87 | ---- 88 | 89 | Value types: 90 | * `redirect` - use standard server side authorization 91 | * `transparent` - use transparent cookie based authorization 92 | * `cookieDirect` - use manual cookie based authorization 93 | * `json` - use JSON authorization 94 | 95 | -------------------------------------------------------------------------------- /gradle/plugin.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { url 'https://repo.grails.org/grails/core' } 4 | } 5 | dependencies { 6 | classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3' 7 | classpath 'org.asciidoctor:asciidoctorj-epub3:1.5.0-alpha.6' 8 | classpath 'org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.11' 9 | } 10 | } 11 | 12 | apply from: 'gradle/common.gradle' 13 | 14 | apply plugin: 'maven-publish' 15 | apply plugin: 'spring-boot' 16 | apply plugin: 'org.grails.grails-plugin' 17 | apply plugin: 'org.asciidoctor.convert' 18 | 19 | apply from: 'gradle/grailsCentralPublishing.gradle' 20 | apply from: 'https://raw.githubusercontent.com/grails/grails-profile-repository/master/profiles/plugin/templates/bintrayPublishing.gradle' 21 | 22 | group = 'org.grails.plugins' 23 | sourceCompatibility = targetCompatibility = 1.7 24 | 25 | def setIfNotSet = { String name, value -> 26 | if (!project.ext.has(name)) { 27 | project.ext[name] = value 28 | } 29 | } 30 | setIfNotSet 'issueTrackerUrl', project.vcsUrl + '/issues' 31 | setIfNotSet 'websiteUrl', project.vcsUrl 32 | 33 | dependencies { 34 | provided 'javax.servlet:javax.servlet-api:3.1.0' 35 | provided 'org.grails:grails-dependencies' 36 | provided 'org.grails:grails-web-boot' 37 | provided 'org.springframework.boot:spring-boot-starter-logging' 38 | } 39 | 40 | def asciidoctorAttributes = [ 41 | copyright : 'Apache License, Version 2.0', 42 | docinfo1 : 'true', 43 | doctype : 'book', 44 | encoding : 'utf-8', 45 | // 'front-cover-image' : 'image:cover.png[Front Cover,800,600]', 46 | icons : 'font', 47 | id : project.name + ':' + project.version, 48 | idprefix : '', 49 | idseparator : '-', 50 | lang : 'en', 51 | linkattrs : true, 52 | numbered : '', 53 | producer : 'Asciidoctor', 54 | revnumber : project.version, 55 | setanchors : true, 56 | 'source-highlighter' : 'prettify', 57 | toc : 'left', 58 | toc2 : '', 59 | toclevels : '2' 60 | ] 61 | 62 | import org.asciidoctor.gradle.AsciidoctorTask 63 | 64 | tasks.withType(AsciidoctorTask) { 65 | attributes asciidoctorAttributes 66 | outputDir new File(buildDir, 'docs') 67 | separateOutputDirs = false 68 | sourceDir = file('src/docs') 69 | sources { 70 | include 'index.adoc' 71 | } 72 | } 73 | 74 | task getKindlegen { 75 | File tgz = file('kindlegen/kindlegen.tar.gz') 76 | if (!tgz.exists()) { 77 | tgz.parentFile.mkdirs() 78 | String url = 'http://kindlegen.s3.amazonaws.com/kindlegen_linux_2.6_i386_v2_9.tar.gz' 79 | println "Downloading Kindlegen from $url" 80 | ant.get(src: url, dest: tgz) 81 | ant.untar(src: tgz, dest: file('kindlegen'), compression: 'gzip') 82 | ant.chmod(file: file('kindlegen/kindlegen'), perm: '+x') 83 | } 84 | } 85 | 86 | task asciidoc(type: AsciidoctorTask, description: 'Generates single-page HTML, PDF, and EPUB3') { 87 | backends 'html5', 'pdf', 'epub3' 88 | } 89 | 90 | task asciidocMobi(type: AsciidoctorTask, description: 'Generates MOBI for Kindle', dependsOn: getKindlegen) { 91 | backends 'epub3' 92 | attributes(['ebook-format': 'kf8'] + asciidoctorAttributes) 93 | } 94 | 95 | task docs(dependsOn: [asciidoc]) << { 96 | 97 | File dir = new File(buildDir, 'docs') 98 | ['epub', 'pdf'].each { String ext -> 99 | File f = new File(dir, 'index.' + ext) 100 | if (f.exists()) { 101 | f.renameTo new File(dir, project.name + '-' + project.version + '.' + ext) 102 | } 103 | } 104 | 105 | new File(buildDir, 'docs/ghpages.html') << file('src/docs/index.tmpl').text.replaceAll('@VERSION@', project.version) 106 | 107 | copy { 108 | from 'src/docs' 109 | into new File(buildDir, 'docs').path 110 | include '**/*.png' 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/docs/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Grails Spring Security Facebook Plugin 8 | 9 | 58 | 59 | 60 | 61 | 62 | 63 | Fork me on GitHub 64 | 65 | 66 |
67 |

Grails Spring Security Facebook Plugin

68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
Version@VERSION@ 73 |
Grails Version3.0.0 > *
AuthorIgor Artamonov
84 | 85 |
86 | 87 |

Current Documentation

88 | 93 | 94 |

Documentation (version 0.17, for Grails 2.x)

95 | 98 | 99 |
100 | 101 | 109 | 110 |

Download Source

111 |

112 | You can download this project in either 113 | zip or 114 | tar formats. 115 |

116 |

You can also clone the project with Git by running: 117 |

$ git clone git://github.com/grails-plugins/grails-spring-security-facebook
118 |

119 | 120 |
121 | 122 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthRedirectFilter.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import javax.servlet.FilterChain 6 | import javax.servlet.ServletException 7 | import javax.servlet.ServletRequest 8 | import javax.servlet.ServletResponse 9 | import javax.servlet.http.HttpServletRequest 10 | import javax.servlet.http.HttpServletResponse 11 | 12 | import grails.web.mapping.LinkGenerator 13 | import org.springframework.security.core.Authentication 14 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter 15 | import org.springframework.security.web.util.UrlUtils 16 | import org.springframework.security.web.util.matcher.RequestMatcher 17 | import org.springframework.util.Assert 18 | 19 | /** 20 | * 21 | * @author Igor Artamonov (http://igorartamonov.com) 22 | * @since 19.09.12 23 | */ 24 | @CompileStatic 25 | class FacebookAuthRedirectFilter extends AbstractAuthenticationProcessingFilter { 26 | 27 | FacebookAuthUtils facebookAuthUtils 28 | LinkGenerator linkGenerator 29 | RequestMatcher redirectFromMatcher 30 | 31 | String redirectFromUrl 32 | String redirectToUrl 33 | 34 | FacebookAuthRedirectFilter(String defaultFilterProcessesUrl) { 35 | super(defaultFilterProcessesUrl) 36 | this.redirectToUrl = defaultFilterProcessesUrl 37 | } 38 | 39 | @Override 40 | void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 41 | if (redirectFromMatcher.matches((HttpServletRequest)request)) { 42 | ((HttpServletResponse)response).sendRedirect( 43 | facebookAuthUtils.prepareRedirectUrl(absoluteRedirectUrl, facebookAuthUtils.requiredPermissions)) 44 | } 45 | else { 46 | super.doFilter(request, response, chain) 47 | } 48 | } 49 | 50 | @Override 51 | Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { 52 | String code = request.getParameter('code') 53 | if (!code) { 54 | throw new InvalidRequestException("Request is empty") 55 | } 56 | 57 | logger.debug("Got 'code' from Facebook. Process authentication using this code") 58 | authenticationManager.authenticate new FacebookAuthToken(code: code, uid: -1L, redirectUri: getAbsoluteRedirectUrl()) 59 | } 60 | 61 | String getAbsoluteRedirectUrl() { 62 | linkGenerator.link(uri: redirectToUrl, absolute: true) 63 | } 64 | 65 | void setRedirectFromUrl(String redirectFromUrl) { 66 | this.redirectFromUrl = redirectFromUrl 67 | this.redirectFromMatcher = new FriendlyFilterProcessUrlRequestMatcher(redirectFromUrl) 68 | } 69 | 70 | //original matcher from Spring Security is private 71 | static final class FriendlyFilterProcessUrlRequestMatcher implements RequestMatcher { 72 | public final String filterProcessesUrl 73 | 74 | FriendlyFilterProcessUrlRequestMatcher(String filterProcessesUrl) { 75 | Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified") 76 | Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), "$filterProcessesUrl isn't a valid redirect URL") 77 | this.filterProcessesUrl = filterProcessesUrl 78 | } 79 | 80 | boolean matches(HttpServletRequest request) { 81 | String uri = request.requestURI 82 | int pathParamIndex = uri.indexOf(';') 83 | 84 | if (pathParamIndex > 0) { 85 | // strip everything after the first semi-colon 86 | uri = uri.substring(0, pathParamIndex) 87 | } 88 | 89 | if (request.contextPath) { 90 | StringBuilder expectedPath = new StringBuilder() 91 | expectedPath.append(request.contextPath).append(filterProcessesUrl) 92 | return uri.endsWith(expectedPath.toString()) 93 | } 94 | return uri.endsWith(filterProcessesUrl) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthCookieTransparentFilter.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import javax.servlet.FilterChain 6 | import javax.servlet.ServletRequest 7 | import javax.servlet.ServletResponse 8 | import javax.servlet.http.Cookie 9 | import javax.servlet.http.HttpServletRequest 10 | import javax.servlet.http.HttpServletResponse 11 | 12 | import org.springframework.context.ApplicationEventPublisher 13 | import org.springframework.context.ApplicationEventPublisherAware 14 | import org.springframework.security.authentication.AuthenticationManager 15 | import org.springframework.security.authentication.BadCredentialsException 16 | import org.springframework.security.core.Authentication 17 | import org.springframework.security.core.context.SecurityContextHolder 18 | import org.springframework.web.filter.GenericFilterBean 19 | 20 | /** 21 | * TODO 22 | * 23 | * @since 14.10.11 24 | * @author Igor Artamonov (http://igorartamonov.com) 25 | */ 26 | @CompileStatic 27 | class FacebookAuthCookieTransparentFilter extends GenericFilterBean implements ApplicationEventPublisherAware { 28 | 29 | ApplicationEventPublisher applicationEventPublisher 30 | FacebookAuthUtils facebookAuthUtils 31 | AuthenticationManager authenticationManager 32 | String logoutUrl = '/j_spring_security_logout' 33 | String forceLoginParameter 34 | String filterProcessUrl 35 | 36 | void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { 37 | HttpServletRequest request = (HttpServletRequest)req 38 | HttpServletResponse response = (HttpServletResponse)res 39 | String url = request.requestURI.substring(request.contextPath.length()) 40 | logger.debug("Processing url: $url") 41 | if (url != logoutUrl && 42 | (!SecurityContextHolder.context.authentication || 43 | (forceLoginParameter && request.getParameter(forceLoginParameter) == 'true'))) { 44 | logger.debug("Applying facebook auth filter") 45 | assert facebookAuthUtils 46 | Cookie cookie = facebookAuthUtils.getAuthCookie(request) 47 | if (cookie) { 48 | if (processCookie(cookie, request, response, chain)) { 49 | return 50 | } 51 | } 52 | else { 53 | logger.debug("No auth cookie") 54 | } 55 | } 56 | else { 57 | logger.debug("SecurityContextHolder not populated with FacebookAuthToken token, as it already contained: $SecurityContextHolder.context.authentication"); 58 | } 59 | 60 | //when not authenticated, dont have auth cookie or bad credentials 61 | chain.doFilter(request, response) 62 | } 63 | 64 | protected boolean processCookie(Cookie cookie, HttpServletRequest request, HttpServletResponse response, FilterChain chain) { 65 | try { 66 | FacebookAuthToken token = facebookAuthUtils.build(cookie.value) 67 | if (!token) { 68 | return false 69 | } 70 | 71 | Authentication authentication 72 | try { 73 | authentication = authenticationManager.authenticate(token) 74 | } 75 | catch (Throwable t) { 76 | logger.warn("Error during authentication. Skipping. Message: $t.message") 77 | } 78 | if (authentication?.authenticated) { 79 | // Store to SecurityContextHolder 80 | SecurityContextHolder.context.authentication = authentication 81 | 82 | if (logger.debugEnabled) { 83 | logger.debug("SecurityContextHolder populated with FacebookAuthToken: '$SecurityContextHolder.context.authentication'"); 84 | } 85 | try { 86 | chain.doFilter(request, response) 87 | return true 88 | } 89 | finally { 90 | SecurityContextHolder.context.authentication = null 91 | } 92 | } 93 | } 94 | catch (BadCredentialsException e) { 95 | logger.info("Invalid cookie, skip. Message was: $e.message") 96 | } 97 | 98 | false 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/docs/customization.adoc: -------------------------------------------------------------------------------- 1 | == Customization 2 | 3 | === How it works 4 | 5 | If you need to add some specific logic to default plugin behaviour you have to create your own 6 | service called `FacebookAuthService`. Plugin will check for know methods of this service, and if 7 | they're exist - use them instead of own. 8 | 9 | It's some kind of extending an abstract class. You don't need to create all methods, just what you need. 10 | 11 | Used objects: 12 | 13 | * - domain class for your facebook user. It's your own class, can have other name, it's just a example 14 | * - general user, used by Spring Security. It's your own class, can have other name, it's just a example 15 | * FacebookAuthToken - token provided by plugin 16 | 17 | and can be same object, or can be two different object (with a relation), depends 18 | on your architecture. 19 | 20 | === Default Implementation 21 | 22 | Please, take a look at sources of [DefaultFacebookAuthDao|https://github.com/splix/grails-spring-security-facebook/blob/master/src/groovy/com/the6hours/grails/springsecurity/facebook/DefaultFacebookAuthDao.groovy] 23 | to understand how it works, and which methods you can use for customization 24 | 25 | ==== List of possible methods: 26 | 27 | ===== void onCreate( user, FacebookAuthToken token) 28 | 29 | Called after user was created by plugin, just before saving into database. You can fill user object 30 | with some extra values. 31 | 32 | Not called if you have implemented method @create(..)@ 33 | 34 | Where: 35 | 36 | * user - your domain for Facebook User 37 | * token - com.the6hours.grails.springsecurity.facebook.FacebookAuthToken 38 | 39 | ===== void afterCreate( user, FacebookAuthToken token) 40 | 41 | Called after user was created by plugin, and when it's already saved into database. 42 | 43 | Not called if you have implemented method @create(..)@ 44 | 45 | Where: 46 | 47 | * user - your domain for Facebook User 48 | * token - com.the6hours.grails.springsecurity.facebook.FacebookAuthToken 49 | 50 | ===== findUser(long uid) 51 | 52 | Called when facebook user is authenticated (on every request), must return existing instance 53 | for specified facebook uid, if exits. If doesn't - return null 54 | 55 | Where: 56 | 57 | * uid - facebook user id 58 | 59 | ===== create(FacebookAuthToken token) 60 | 61 | Called when we have a new facebook user, called on first login to create all required 62 | data structures. 63 | 64 | Where: 65 | 66 | * token - com.the6hours.grails.springsecurity.facebook.FacebookAuthToken 67 | 68 | Notice, that if you have such method, all other method for user creation will no be called: 69 | 70 | * createAppUser 71 | * prepopulateAppUser 72 | * onCreate 73 | * afterCreate 74 | * createRoles 75 | 76 | ===== createAppUser( user, FacebookAuthToken token) 77 | 78 | Called when we have a new facebook user, called on first login to create main app User domain (when 79 | we store Facebook User details in different domain). 80 | 81 | Not called if you have implemented method @create(..)@ 82 | 83 | Where: 84 | 85 | * user - your domain for Facebook User 86 | * token - com.the6hours.grails.springsecurity.facebook.FacebookAuthToken 87 | 88 | ===== void createRoles( user) 89 | 90 | Called when we have a new facebook user, called on first login to create roles list for new user 91 | 92 | Where: 93 | 94 | * user - your domain for Facebook User 95 | 96 | ===== def getPrincipal( user) 97 | 98 | Must return object to store in security context for specified facebook user (can return itself) 99 | 100 | Where: 101 | 102 | * user - your domain for Facebook User 103 | 104 | ===== getFacebookUser( person) 105 | 106 | Must return instance of your domain object for facebook user for specified person (if it's not a same object) 107 | 108 | Where: 109 | 110 | * person - your domain for 111 | 112 | 113 | ===== Collection getRoles( user) 114 | 115 | Must return roles list for specified user 116 | 117 | Where: 118 | 119 | * user - your domain for Facebook User 120 | 121 | ===== void prepopulateAppUser( person, FacebookAuthToken token) 122 | 123 | Must return roles list for specified facebook user 124 | 125 | Where: 126 | 127 | * person - your domain for 128 | * token - com.the6hours.grails.springsecurity.facebook.FacebookAuthToken 129 | -------------------------------------------------------------------------------- /src/test/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthUtilsSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import org.springframework.security.authentication.BadCredentialsException 4 | import spock.lang.Specification 5 | 6 | import javax.servlet.http.Cookie 7 | import javax.servlet.http.HttpServletRequest 8 | 9 | /** 10 | * 11 | * Since 25.04.13 12 | * @author Igor Artamonov, http://igorartamonov.com 13 | */ 14 | class FacebookAuthUtilsSpec extends Specification { 15 | 16 | // Test tools: 17 | // 18 | // https://developers.facebook.com/tools/echo 19 | // http://fbapp.herokuapp.com/ 20 | // 21 | 22 | FacebookAuthUtils facebookAuthUtils 23 | 24 | def setup() { 25 | facebookAuthUtils = new FacebookAuthUtils( 26 | secret: 'test_secret', 27 | applicationId: 1000 28 | ) 29 | } 30 | 31 | def "Can parse valid signed_request"() { 32 | expect: 33 | facebookAuthUtils.extractSignedJson(signed_request) 34 | where: 35 | signed_request << [ 36 | 'HtEfeVZxRwIk1L7cT5cp9dKL2BGo49+CNNkteAROooE.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsInVzZXJfaWQiOiIxIn0', 37 | 'sgVOceUTD7kvvJeBt8cQuJlts24wr7veakS-fQ0pmcc.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZF9hdCI6MTI4ODk0NzkxOSwidGhlIGFuc3dlciI6NDJ9', 38 | 'D/XUCLgN8NWk7H4bgjSa7o+S9IFwHoLOnCL4aDJwSh0.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsInVzZXJfaWQiOiIxIiwiY29kZSI6ImFzZGZramhhc2Zhc2praGFkc2poa2dhc2Rqa2hnMzRqaGFqaGdhc2RqaGdhZHNmamtoZ2FzZGtqaGczNGtqaGdhc2RqaGdmdmpiIn0' 39 | ] 40 | } 41 | 42 | def "Throw exception on invalid signed_request"() { 43 | when: 44 | facebookAuthUtils.extractSignedJson(signed_request) 45 | then: 46 | BadCredentialsException e = thrown(BadCredentialsException) 47 | e.message == 'Invalid signature' 48 | where: 49 | signed_request << [ 50 | 'EfeVZxRwIk1L7cT5cp9dKL2BGo49+CNNkteAROooE.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsInVzZXJfaWQiOiIxIn0', 51 | 'ssssgVOceUTD7kvvJeBt8cQuJlts24wr7veakS-fQ0pmcc.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZF9hdCI6MTI4ODk0NzkxOSwidGhlIGFuc3dlciI6NDJ9', 52 | ] 53 | } 54 | 55 | def "Extract correct json"() { 56 | setup: 57 | String signed_request = 'HtEfeVZxRwIk1L7cT5cp9dKL2BGo49+CNNkteAROooE.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsInVzZXJfaWQiOiIxIn0' 58 | when: 59 | def json = facebookAuthUtils.extractSignedJson(signed_request) 60 | then: 61 | json != null 62 | json.user_id == '1' 63 | json.algorithm == "HMAC-SHA256" 64 | } 65 | 66 | def "Verify valid signature"() { 67 | expect: 68 | facebookAuthUtils.verifySign(signature, payload) 69 | where: 70 | signature | payload 71 | 'HtEfeVZxRwIk1L7cT5cp9dKL2BGo49+CNNkteAROooE' | 'eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsInVzZXJfaWQiOiIxIn0' 72 | 'HtEfeVZxRwIk1L7cT5cp9dKL2BGo49-CNNkteAROooE' | 'eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsInVzZXJfaWQiOiIxIn0' 73 | } 74 | 75 | def "Encode Params"() { 76 | expect: 77 | query == facebookAuthUtils.encodeParams(data) 78 | where: 79 | data | query 80 | [foo: 'bar'] | 'foo=bar' 81 | [foo: 5151] | 'foo=5151' 82 | [foo: 1, bar: 'baz'] | 'foo=1&bar=baz' 83 | [foo: 'привет', bar: '100/7%3'] | 'foo=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82&bar=100%2F7%253' 84 | } 85 | 86 | def "Return null if no cookies"() { 87 | setup: 88 | def request = Mock(HttpServletRequest) 89 | when: 90 | def act = facebookAuthUtils.getAuthCookie(request) 91 | then: 92 | 1 * request.getCookies() >> null 93 | act == null 94 | } 95 | 96 | def "Get cookie if exists"() { 97 | setup: 98 | def request = Mock(HttpServletRequest) 99 | facebookAuthUtils.applicationId = '123456' 100 | def cookies = [ 101 | new Cookie('foo', 'bar'), 102 | new Cookie('fbsr_8888888', 'incorrect cookie'), 103 | new Cookie('fbsr_123456', 'correct cookie'), 104 | ] 105 | when: 106 | def act = facebookAuthUtils.getAuthCookie(request) 107 | then: 108 | _ * request.getCookies() >> cookies 109 | act != null 110 | act.value == 'correct cookie' 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/JsonAuthenticationHandler.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import grails.converters.JSON 4 | import groovy.transform.CompileStatic 5 | import groovy.transform.TypeCheckingMode 6 | import org.springframework.security.core.GrantedAuthority 7 | 8 | import javax.servlet.ServletException 9 | import javax.servlet.ServletOutputStream 10 | import javax.servlet.http.HttpServletRequest 11 | import javax.servlet.http.HttpServletResponse 12 | 13 | import org.slf4j.Logger 14 | import org.slf4j.LoggerFactory 15 | import org.springframework.beans.factory.InitializingBean 16 | import org.springframework.context.ApplicationContext 17 | import org.springframework.context.ApplicationContextAware 18 | import org.springframework.security.core.Authentication 19 | import org.springframework.security.core.AuthenticationException 20 | import org.springframework.security.core.userdetails.UserDetails 21 | import org.springframework.security.web.authentication.AuthenticationFailureHandler 22 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler 23 | 24 | /** 25 | * 26 | * @author Igor Artamonov (http://igorartamonov.com) 27 | * @since 25.01.13 28 | */ 29 | @CompileStatic 30 | class JsonAuthenticationHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler, InitializingBean, ApplicationContextAware { 31 | 32 | private static final Logger log = LoggerFactory.getLogger(this) 33 | 34 | ApplicationContext applicationContext 35 | 36 | boolean useJsonp = false 37 | boolean defaultJsonpCallback = 'jsonpCallback' 38 | def facebookAuthService 39 | 40 | void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) 41 | throws IOException, ServletException { 42 | 43 | response.status = HttpServletResponse.SC_UNAUTHORIZED 44 | Map data = [authenticated: false, message: exception?.message] 45 | 46 | data = callAuthServiceOnJsonFailure(data, exception) 47 | JSON json = new JSON(data) 48 | if (useJsonp) { 49 | renderAsJSONP(json, request, response) 50 | } else { 51 | json.render(response) 52 | } 53 | } 54 | 55 | @CompileStatic(TypeCheckingMode.SKIP) 56 | protected Map callAuthServiceOnJsonFailure(Map data, AuthenticationException exception) { 57 | if (facebookAuthService?.respondsTo('onJsonFailure')) { 58 | def data2 = facebookAuthService.onJsonFailure(data, exception) 59 | if (data2) { 60 | data = data2 61 | } 62 | } 63 | data 64 | } 65 | 66 | void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) 67 | throws IOException, ServletException { 68 | 69 | FacebookAuthToken token = (FacebookAuthToken)authentication 70 | Map data = [authenticated: true, uid: token.uid, roles: token.authorities?.collect { GrantedAuthority it -> it.authority }] 71 | if (token.principal instanceof UserDetails) { 72 | data.username = ((UserDetails)token.principal).username 73 | data.enabled = ((UserDetails)token.principal).enabled 74 | } 75 | data = callAuthServiceOnJsonSuccess(data, authentication) 76 | JSON json = new JSON(data) 77 | if (useJsonp) { 78 | renderAsJSONP(json, request, response) 79 | } else { 80 | json.render(response) 81 | } 82 | } 83 | 84 | @CompileStatic(TypeCheckingMode.SKIP) 85 | protected Map callAuthServiceOnJsonSuccess(Map data, Authentication authentication) { 86 | if (facebookAuthService?.respondsTo('onJsonSuccess')) { 87 | def data2 = facebookAuthService.onJsonSuccess(data, authentication) 88 | if (data2) { 89 | data = data2 90 | } 91 | } 92 | data 93 | } 94 | 95 | void renderAsJSONP(JSON json, HttpServletRequest request, HttpServletResponse response) { 96 | String callback = defaultJsonpCallback 97 | if (request.getParameterMap().containsKey('callback')) { 98 | callback = request.getParameter('callback') 99 | } else if (request.getParameterMap().containsKey('jsonp')) { 100 | callback = request.getParameter('jsonp') 101 | } 102 | 103 | String jsonString = json.toString() 104 | 105 | response.setContentType('application/javascript') 106 | response.setContentLength(callback.bytes.length + 'c'.bytes.length * 2 + jsonString.bytes.length) 107 | response.outputStream << callback << '(' << jsonString << ')' 108 | } 109 | 110 | void afterPropertiesSet() { 111 | if (facebookAuthService) { 112 | return 113 | } 114 | 115 | if (applicationContext.containsBean('facebookAuthService')) { 116 | log.debug("Use provided facebookAuthService") 117 | facebookAuthService = applicationContext.getBean('facebookAuthService') 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /grails-app/conf/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | grails: 3 | profile: web-plugin 4 | codegen: 5 | defaultPackage: grails.spring.security.facebook 6 | info: 7 | app: 8 | name: '@info.app.name@' 9 | version: '@info.app.version@' 10 | grailsVersion: '@info.app.grailsVersion@' 11 | spring: 12 | groovy: 13 | template: 14 | check-template-location: false 15 | 16 | --- 17 | 18 | grails: 19 | plugin: 20 | security: 21 | facebook: 22 | appId: Invalid 23 | secret: Invalid 24 | apiKey: Invalid 25 | 26 | domain: 27 | classname: FacebookUser 28 | appUserConnectionPropertyName: user 29 | 30 | useAjax: true 31 | autoCheck: true 32 | 33 | jsconf: fbSecurity 34 | 35 | #see http://developers.facebook.com/docs/authentication/permissions/ 36 | permissions: 37 | - email 38 | 39 | taglib: 40 | language: en_US 41 | button: 42 | text: Login with Facebook 43 | defaultImg: /images/connect.png 44 | initfb: true 45 | 46 | autoCreate: 47 | enabled: true 48 | roles: 49 | - ROLE_USER 50 | - ROLE_FACEBOOK 51 | 52 | filter: 53 | json: 54 | processUrl: /j_spring_security_facebook_json 55 | type: json #or 'jsonp' 56 | methods: 57 | - POST 58 | redirect: 59 | redirectFromUrl: /j_spring_security_facebook_redirect 60 | processUrl: /j_spring_security_facebook_check 61 | type: redirect #transparent, cookieDirect, redirect or json 62 | position: 720 #see SecurityFilterPosition 63 | forceLoginParameter: j_spring_facebook_force 64 | 65 | beans: 66 | #successHandler: 67 | #failureHandler: 68 | #redirectSuccessHandler: 69 | #redirectFailureHandler: 70 | 71 | --- 72 | grails: 73 | mime: 74 | disable: 75 | accept: 76 | header: 77 | userAgents: 78 | - Gecko 79 | - WebKit 80 | - Presto 81 | - Trident 82 | types: 83 | all: '*/*' 84 | atom: application/atom+xml 85 | css: text/css 86 | csv: text/csv 87 | form: application/x-www-form-urlencoded 88 | html: 89 | - text/html 90 | - application/xhtml+xml 91 | js: text/javascript 92 | json: 93 | - application/json 94 | - text/json 95 | multipartForm: multipart/form-data 96 | pdf: application/pdf 97 | rss: application/rss+xml 98 | text: text/plain 99 | hal: 100 | - application/hal+json 101 | - application/hal+xml 102 | xml: 103 | - text/xml 104 | - application/xml 105 | urlmapping: 106 | cache: 107 | maxsize: 1000 108 | controllers: 109 | defaultScope: singleton 110 | converters: 111 | encoding: UTF-8 112 | views: 113 | default: 114 | codec: html 115 | gsp: 116 | encoding: UTF-8 117 | htmlcodec: xml 118 | codecs: 119 | expression: html 120 | scriptlets: html 121 | taglib: none 122 | staticparts: none 123 | --- 124 | hibernate: 125 | cache: 126 | queries: false 127 | use_second_level_cache: true 128 | use_query_cache: false 129 | region.factory_class: 'org.hibernate.cache.ehcache.EhCacheRegionFactory' 130 | 131 | endpoints: 132 | jmx: 133 | unique-names: true 134 | 135 | dataSource: 136 | pooled: true 137 | jmxExport: true 138 | driverClassName: org.h2.Driver 139 | username: sa 140 | password: 141 | 142 | environments: 143 | development: 144 | dataSource: 145 | dbCreate: create-drop 146 | url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 147 | test: 148 | dataSource: 149 | dbCreate: update 150 | url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 151 | production: 152 | dataSource: 153 | dbCreate: update 154 | url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 155 | properties: 156 | jmxEnabled: true 157 | initialSize: 5 158 | maxActive: 50 159 | minIdle: 5 160 | maxIdle: 25 161 | maxWait: 10000 162 | maxAge: 600000 163 | timeBetweenEvictionRunsMillis: 5000 164 | minEvictableIdleTimeMillis: 60000 165 | validationQuery: SELECT 1 166 | validationQueryTimeout: 3 167 | validationInterval: 15000 168 | testOnBorrow: true 169 | testWhileIdle: true 170 | testOnReturn: false 171 | jdbcInterceptors: ConnectionState 172 | defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED 173 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /grails-app/taglib/com/the6hours/grails/springsecurity/facebook/FacebookAuthTagLib.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import grails.plugin.springsecurity.SpringSecurityUtils 4 | 5 | /** 6 | * TODO 7 | * 8 | * @since 31.03.11 9 | * @author Igor Artamonov (http://igorartamonov.com) 10 | */ 11 | class FacebookAuthTagLib { 12 | 13 | static namespace = 'facebookAuth' 14 | 15 | static final String MARKER = 'com.the6hours.grails.springsecurity.facebook.FacebookAuthTagLib#init' 16 | 17 | FacebookAuthUtils facebookAuthUtils 18 | 19 | /** 20 | * Add Facebook Javascript SDK initialization code. You could also provide extra initialization JS in the body of 21 | * this tag, it will be executed just after Facebook SDK initialization. 22 | * 23 | * By default tag executed only once pre page 24 | * 25 | * @attr force Force tag to put FB SDK initialization code (even if it's already added) 26 | * 27 | */ 28 | Closure init = { attrs, body -> 29 | Boolean init = request.getAttribute(MARKER) ?: false 30 | 31 | def conf = SpringSecurityUtils.securityConfig.facebook 32 | if (conf.taglib?.initfb == false) { 33 | log.debug("FB Init is disabled. Skip") 34 | return 35 | } 36 | 37 | if (!init || attrs.force == 'true') { 38 | String lang = conf.taglib.language 39 | if (attrs.lang) { 40 | lang = attrs.lang 41 | } else if (attrs.language) { 42 | lang = attrs.language 43 | } 44 | def appId = conf.appId 45 | out << '
\n' 46 | 47 | out << '\n' 71 | 72 | request.setAttribute(MARKER, true) 73 | } 74 | } 75 | 76 | /** 77 | * Put Facebook Connect button. 78 | * 79 | * @emptyTag 80 | * 81 | * @attr type - 'server' or 'client', depends on what type of authorization you would like to use. By default it 82 | * uses server-side authentication (if such authentication type is enabled) 83 | * @attr img - url to image for connect button (for server-side authentication only) 84 | */ 85 | Closure connect = { attrs, body -> 86 | def writer = getOut() 87 | if (attrs.type) { 88 | if (attrs.type == 'server') { 89 | writer << serverSideConnect(attrs, body) 90 | return 91 | } 92 | if (attrs.type == 'client') { 93 | writer << clientSideConnect(attrs, body) 94 | return 95 | } 96 | log.error("Invalid connect type: ${attrs.type}") 97 | } 98 | 99 | if (facebookAuthUtils.filterTypes.contains('redirect')) { 100 | log.debug("Do default server-side authentication redirect") 101 | writer << serverSideConnect(attrs, body) 102 | return 103 | } 104 | 105 | log.debug("Do default client-side authentication") 106 | writer << clientSideConnect(attrs, body) 107 | } 108 | 109 | Closure serverSideConnect = { attrs, body -> 110 | log.debug("Apply server side connect") 111 | def writer = getOut() 112 | def conf = SpringSecurityUtils.securityConfig.facebook 113 | String target = attrs.startUrl ?: conf.filter.redirect.redirectFromUrl 114 | String bodyValue = body() 115 | if (!bodyValue || !bodyValue.trim()) { 116 | String imgUrl 117 | if (attrs.img) { 118 | imgUrl = attrs.img 119 | } else { 120 | imgUrl = resource( 121 | dir: 'images', 122 | file: 'connect.png', 123 | plugin: 'spring-security-facebook' 124 | ) 125 | } 126 | bodyValue = img(attrs, imgUrl) 127 | } 128 | Closure newBody = { 129 | return bodyValue 130 | } 131 | writer << link([uri: target], newBody) 132 | } 133 | 134 | Closure clientSideConnect = { attrs, body -> 135 | def conf = SpringSecurityUtils.securityConfig.facebook 136 | 137 | if (attrs.skipInit == null || !Boolean.valueOf(attrs.skipInit)) { 138 | init(attrs, body) 139 | } 140 | 141 | String buttonText = conf.taglib.button.text 142 | if (attrs.text) { 143 | buttonText = attrs.text 144 | } 145 | 146 | List permissions = [] 147 | def rawPermissions 148 | if (attrs.permissions) { 149 | rawPermissions = attrs.permissions 150 | } else { 151 | rawPermissions = facebookAuthUtils.requiredPermissions 152 | } 153 | if (rawPermissions) { 154 | if (rawPermissions instanceof Collection) { 155 | permissions = rawPermissions.findAll { it }.collect { it.toString().trim() }.findAll { it } 156 | } else { 157 | permissions = rawPermissions.toString().split(',').collect { it.trim() } 158 | } 159 | } else { 160 | log.debug("Permissions aren't configured") 161 | } 162 | 163 | boolean showFaces = false 164 | 165 | out << "" 166 | } 167 | 168 | private String img(Map attrs, String src) { 169 | def conf = SpringSecurityUtils.securityConfig.facebook 170 | 171 | StringBuilder buf = new StringBuilder() 172 | buf.append(' 175 | if (attr.startsWith('img-')) { 176 | attr = attr.substring('img-'.length()) 177 | used[attr] = value?.toString() 178 | } 179 | } 180 | if (!used.alt) { 181 | used.alt = conf.taglib.button.text 182 | } 183 | used.each { key, value -> buf.append(key).append('="').append(value).append('" ') } 184 | buf.append("/>") 185 | buf 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthProvider.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import org.apache.commons.lang.StringUtils 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory 8 | import org.springframework.beans.factory.InitializingBean 9 | import org.springframework.context.ApplicationContext 10 | import org.springframework.context.ApplicationContextAware 11 | import org.springframework.security.authentication.AuthenticationProvider 12 | import org.springframework.security.authentication.CredentialsExpiredException 13 | import org.springframework.security.core.Authentication 14 | import org.springframework.security.core.GrantedAuthority 15 | import org.springframework.security.core.userdetails.UserDetails 16 | import org.springframework.security.core.userdetails.UserDetailsChecker 17 | import org.springframework.security.core.userdetails.UsernameNotFoundException 18 | 19 | @CompileStatic 20 | class FacebookAuthProvider implements AuthenticationProvider, InitializingBean, ApplicationContextAware { 21 | 22 | private static final Logger log = LoggerFactory.getLogger(this) 23 | 24 | ApplicationContext applicationContext 25 | FacebookAuthDao facebookAuthDao 26 | FacebookAuthUtils facebookAuthUtils 27 | UserDetailsChecker postAuthenticationChecks 28 | 29 | def facebookAuthService 30 | 31 | boolean createNew = true 32 | 33 | Authentication authenticate(Authentication authentication) { 34 | FacebookAuthToken token = (FacebookAuthToken)authentication 35 | 36 | if (token.uid <= 0) { 37 | if (StringUtils.isEmpty(token.code) && token.accessToken == null) { 38 | log.error("Token should contain 'code' OR 'accessToken' to get uid") 39 | token.authenticated = false 40 | return token 41 | } 42 | if (token.code) { 43 | token.accessToken = facebookAuthUtils.getAccessToken(token.code, token.redirectUri) 44 | if (token.accessToken == null) { 45 | log.error("Can't fetch access_token for code '$token.code'") 46 | token.authenticated = false 47 | return token 48 | } 49 | } 50 | token.uid = facebookAuthUtils.loadUserUid(token.accessToken.accessToken) 51 | if (token.uid <= 0) { 52 | log.error("Can't fetch uid") 53 | token.authenticated = false 54 | return token 55 | } 56 | } 57 | 58 | def user = facebookAuthDao.findUser(token.uid as Long) 59 | boolean justCreated = false 60 | 61 | if (!user) { 62 | //log.debug "New person $token.uid" 63 | if (createNew) { 64 | log.info "Create new facebook user with uid $token.uid" 65 | if (token.accessToken == null) { 66 | token.accessToken = facebookAuthUtils.getAccessToken(token.code, token.redirectUri) 67 | } 68 | if (token.accessToken == null) { 69 | log.error("Can't create user w/o access_token") 70 | throw new CredentialsExpiredException("Can't receive access_token from Facebook") 71 | } 72 | user = facebookAuthDao.create(token) 73 | justCreated = true 74 | } else { 75 | handleUserNotFoundNoAutocreate(token) 76 | } 77 | } 78 | 79 | if (user) { 80 | if (justCreated) { 81 | log.debug("User is just created") 82 | } 83 | if (!justCreated && token.accessToken != null) { 84 | log.debug("Set new access token for user $user") 85 | facebookAuthDao.updateToken(user, token) 86 | } 87 | if (!facebookAuthDao.hasValidToken(user)) { 88 | log.debug("User $user has invalid access token") 89 | String currentAccessToken = facebookAuthDao.getAccessToken(user) 90 | FacebookAccessToken freshToken = null 91 | if (currentAccessToken) { 92 | try { 93 | log.debug("Refresh access token for $user") 94 | freshToken = facebookAuthUtils.refreshAccessToken(currentAccessToken) 95 | if (!freshToken) { 96 | log.warn("Can't refresh access token for user $user") 97 | } 98 | } catch (IOException e) { 99 | log.warn("Can't refresh access token for user $user") 100 | } 101 | } 102 | 103 | if (!freshToken) { 104 | log.debug("Load a new access token, from code") 105 | freshToken = facebookAuthUtils.getAccessToken(token.code, token.redirectUri) 106 | } 107 | 108 | if (freshToken) { 109 | if (freshToken.accessToken != currentAccessToken) { 110 | log.debug("Update access token for user $user") 111 | token.accessToken = freshToken 112 | facebookAuthDao.updateToken(user, token) 113 | } else { 114 | log.debug("User $user already have same access token") 115 | } 116 | } else { 117 | log.error("Can't update accessToken from Facebook, current token is expired. Disable current authentication") 118 | token.authenticated = false 119 | return token 120 | } 121 | } 122 | 123 | def appUser = facebookAuthDao.getAppUser(user) 124 | def principal = facebookAuthDao.getPrincipal(appUser) 125 | if (principal instanceof UserDetails && postAuthenticationChecks) { 126 | postAuthenticationChecks.check(principal) 127 | } 128 | 129 | token.details = null 130 | token.principal = principal 131 | token.authenticated = true 132 | if (principal instanceof UserDetails) { 133 | token.authorities = (Collection)((UserDetails)principal).authorities 134 | } else { 135 | token.authorities = facebookAuthDao.getRoles(appUser) 136 | } 137 | 138 | } else { 139 | token.authenticated = false 140 | } 141 | token 142 | } 143 | 144 | protected void handleUserNotFoundNoAutocreate(FacebookAuthToken token) throws UsernameNotFoundException { 145 | log.error "User $token.uid doesn't exist, and creation of a new user is disabled." 146 | log.debug "To enable auto creation of users set `grails.plugin.springsecurity.facebook.autoCreate.enabled` to true" 147 | throw new UsernameNotFoundException("Facebook user with uid $token.uid doesn't exist") 148 | } 149 | 150 | boolean supports(Class authentication) { 151 | FacebookAuthToken.isAssignableFrom authentication 152 | } 153 | 154 | void afterPropertiesSet() { 155 | if (facebookAuthService) { 156 | return 157 | } 158 | 159 | if (applicationContext.containsBean('facebookAuthService')) { 160 | log.debug("Use provided facebookAuthService") 161 | facebookAuthService = applicationContext.getBean('facebookAuthService') 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/FacebookAuthUtils.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import grails.converters.JSON 4 | import groovy.transform.CompileStatic 5 | 6 | import java.util.concurrent.TimeUnit 7 | 8 | import javax.crypto.Mac 9 | import javax.crypto.spec.SecretKeySpec 10 | import javax.servlet.http.Cookie 11 | import javax.servlet.http.HttpServletRequest 12 | 13 | import org.apache.commons.codec.binary.Base64 14 | import org.grails.web.json.JSONException 15 | import org.grails.web.json.JSONObject 16 | import org.slf4j.Logger 17 | import org.slf4j.LoggerFactory 18 | import org.springframework.security.authentication.BadCredentialsException 19 | 20 | /** 21 | * TODO 22 | * 23 | * @since 14.10.11 24 | * @author Igor Artamonov (http://igorartamonov.com) 25 | */ 26 | @CompileStatic 27 | class FacebookAuthUtils { 28 | 29 | private static Logger log = LoggerFactory.getLogger(this) 30 | 31 | private static Random RND = new Random() 32 | private int seq = 0 33 | 34 | String apiKey 35 | String secret 36 | String applicationId 37 | 38 | String apiVersion 39 | 40 | List filterTypes = [] 41 | List requiredPermissions = [] 42 | 43 | FacebookAuthToken build(String signedRequest) { 44 | if (!signedRequest) { 45 | return null 46 | } 47 | 48 | JSONObject json = extractSignedJson(signedRequest) 49 | 50 | String code = json.code?.toString() 51 | 52 | FacebookAuthToken token = new FacebookAuthToken( 53 | uid: Long.parseLong(json.user_id.toString()), 54 | code: code 55 | ) 56 | token.authenticated = true 57 | return token 58 | } 59 | 60 | JSONObject extractSignedJson(String signedRequest) { 61 | String[] signedRequestParts = signedRequest.split('\\.') 62 | if (signedRequestParts.length != 2) { 63 | throw new BadCredentialsException("Invalid Signed Request") 64 | } 65 | 66 | String jsonData = new String(Base64.decodeBase64(signedRequestParts[1].bytes), 'UTF-8') 67 | jsonData = jsonData.trim() 68 | JSONObject json 69 | try { 70 | if (!jsonData.endsWith('}')) { 71 | log.info("Seems that Facebook cookie contains corrupted value. SignedRequest: ${signedRequestParts[1]}") 72 | jsonData += '}' 73 | } 74 | json = (JSONObject)JSON.parse(jsonData) 75 | } catch (JSONException e) { 76 | log.error("Cannot parse Facebook cookie. If you're sure that it should be valid cookie, please send '${signedRequestParts[1]}' to plugin author (igor@artamonov.ru)", e) 77 | throw new BadCredentialsException("Invalid cookie format") 78 | } 79 | 80 | if (json.algorithm != 'HMAC-SHA256') { 81 | throw new BadCredentialsException("Unknown hashing algorithm: $json.algorithm") 82 | } 83 | 84 | //log.debug("Payload: $jsonData") 85 | 86 | if (!verifySign(signedRequestParts[0], signedRequestParts[1])) { 87 | throw new BadCredentialsException("Invalid signature") 88 | } 89 | //log.debug "Signature is ok" 90 | json 91 | } 92 | 93 | Cookie getAuthCookie(HttpServletRequest request) { 94 | String cookieName = "fbsr_" + applicationId 95 | request.cookies?.find { Cookie it -> 96 | //FacebookAuthUtils.log.debug("Cookie $it.name, expected $cookieName") 97 | it.name == cookieName 98 | } 99 | } 100 | 101 | long loadUserUid(String accessToken) { 102 | String loadUrl = getVersionedUrl("https://graph.facebook.com/me?access_token=${encode(accessToken)}") 103 | try { 104 | URL url = new URL(loadUrl) 105 | JSONObject json = (JSONObject)JSON.parse(url.readLines().first()) 106 | return json.id as Long 107 | } catch (IOException e) { 108 | log.error("Can't read data from Facebook", e) 109 | } catch (JSONException e) { 110 | log.error("Invalid response", e) 111 | } 112 | return -1 113 | } 114 | 115 | FacebookAccessToken refreshAccessToken(String existingAccessToken) { 116 | Map params = [ 117 | client_id: applicationId, 118 | client_secret: secret, 119 | grant_type: 'fb_exchange_token', 120 | fb_exchange_token: existingAccessToken 121 | ] 122 | requestAccessToken getVersionedUrl("https://graph.facebook.com/oauth/access_token?" + encodeParams(params)) 123 | } 124 | 125 | FacebookAccessToken getAccessToken(String code, String redirectUri = '') { 126 | if (redirectUri == null) { 127 | redirectUri = '' 128 | } 129 | Map params = [ 130 | client_id: applicationId, 131 | redirect_uri: redirectUri, 132 | client_secret: secret, 133 | code: code 134 | ] 135 | requestAccessToken getVersionedUrl("https://graph.facebook.com/oauth/access_token?" + encodeParams(params)) 136 | } 137 | 138 | FacebookAccessToken requestAccessToken(String authUrl) { 139 | try { 140 | URL url = new URL(authUrl) 141 | def response = url.readLines().first() 142 | 143 | // Facebook api response format has changed in v2.3 from Url to JSON 144 | def slurper = new groovy.json.JsonSlurper() 145 | Map data = [:] 146 | data = (Map)slurper.parseText(response) 147 | 148 | /*String response = url.readLines().first() 149 | //println "AccessToken response: $response" 150 | Map data = [:] 151 | response.split('&').each { String part -> 152 | String[] kv = part.split('=') 153 | if (kv.length != 2) { 154 | log.warn("Invalid response part: $part") 155 | } else { 156 | data[kv[0]] = kv[1] 157 | } 158 | }*/ 159 | FacebookAccessToken token = new FacebookAccessToken() 160 | if (data.access_token) { 161 | token.accessToken = data.access_token 162 | } else { 163 | log.error("No access_token in response: $response") 164 | } 165 | if (data.expires_in) { 166 | if (data.expires_in =~ /^\d+$/) { 167 | token.expireAt = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(data.expires_in as Long)) 168 | } else { 169 | log.warn("Invalid 'expires' value: $data.expires_in") 170 | } 171 | } else { 172 | log.error("No expires in response: $response") 173 | } 174 | //log.debug("Got AccessToken: $token") 175 | return token 176 | } catch (IOException e) { 177 | //TODO process error response 178 | log.warn("Can't read data from Facebook", e) 179 | return null 180 | } 181 | } 182 | 183 | boolean verifySign(String sign, String payload) { 184 | if (!sign) { 185 | log.error("Empty signature") 186 | return false 187 | } 188 | if (!payload) { 189 | log.error("Empty payload") 190 | return false 191 | } 192 | String signer = 'HMACSHA256' 193 | //log.debug("Secret $secret") 194 | SecretKeySpec sks = new SecretKeySpec(secret.bytes, signer) 195 | //log.debug("Payload1: `$payload`") 196 | payload = payload.replaceAll("-", "+").replaceAll("_", "/").trim() 197 | //log.debug("Payload2: `$payload`") 198 | sign = sign.replaceAll("-", "+").replaceAll("_", "/") 199 | try { 200 | Mac mac = Mac.getInstance(signer) 201 | mac.init(sks) 202 | byte[] my = mac.doFinal(payload.getBytes('UTF-8')) 203 | byte[] their = Base64.decodeBase64(sign.getBytes('UTF-8')) 204 | //log.info("My: ${new String(Base64.encodeBase64(my, false))}, their: ${new String(Base64.encodeBase64(their))} / $sign") 205 | return Arrays.equals(my, their) 206 | } catch (e) { 207 | log.error("Can't validate signature", e) 208 | false 209 | } 210 | } 211 | 212 | String prepareRedirectUrl(String authPath, List scope = []) { 213 | if (seq >= Integer.MAX_VALUE - 10000) { 214 | seq = 0 215 | } 216 | Map data = [ 217 | client_id: applicationId, 218 | redirect_uri: authPath, 219 | scope: scope.join(','), 220 | state: [seq++, RND.nextInt(1000000)].collect {Integer it -> Integer.toHexString(it)}.join('-') 221 | ] 222 | log.debug("Redirect to ${data.redirect_uri}") 223 | getVersionedUrl("https://www.facebook.com/dialog/oauth?" + encodeParams(data)) 224 | } 225 | 226 | String encodeParams(Map params) { 227 | params.collect { Object key, Object value -> 228 | encode(key) + '=' + encode(value ?: '') 229 | }.join('&') 230 | } 231 | 232 | private String getVersionedUrl(String url) { 233 | apiVersion ? url.replace('facebook.com/',"facebook.com/${apiVersion}/") : url 234 | } 235 | 236 | protected String encode(Object s) { 237 | URLEncoder.encode s.toString(), 'UTF-8' 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/test/groovy/com/the6hours/grails/springsecurity/facebook/DefaultFacebookAuthDaoSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import grails.core.DefaultGrailsApplication 4 | import grails.core.DefaultGrailsClass 5 | import grails.core.GrailsApplication 6 | import grails.core.GrailsClass 7 | import grails.util.Holders 8 | import grails.plugin.springsecurity.SpringSecurityUtils 9 | import org.grails.core.DefaultGrailsDomainClass 10 | import spock.lang.Specification 11 | 12 | import java.sql.Timestamp 13 | 14 | /** 15 | * 16 | * Since 20.04.13 17 | * @author Igor Artamonov, http://igorartamonov.com 18 | */ 19 | class DefaultFacebookAuthDaoSpec extends Specification { 20 | 21 | static { 22 | ExpandoMetaClass.enableGlobally() 23 | } 24 | 25 | DefaultFacebookAuthDao dao 26 | GrailsApplication grails = Mock(GrailsApplication) 27 | Map securityConfig = [:] 28 | 29 | def setup() { 30 | with (grails) { 31 | _ * getArtefact('Domain', TestFacebookUser.canonicalName) >> new DefaultGrailsClass(TestFacebookUser) 32 | _ * getArtefact('Domain', TestAppUser.canonicalName) >> new DefaultGrailsClass(TestAppUser) 33 | _ * getArtefact('Domain', TestAuthority.canonicalName) >> new DefaultGrailsClass(TestAuthority) 34 | _ * getArtefact('Domain', TestRole.canonicalName) >> new DefaultGrailsClass(TestRole) 35 | } 36 | dao = new DefaultFacebookAuthDao( 37 | FacebookUserDomainClazz: TestFacebookUser, 38 | domainClassName: TestFacebookUser.canonicalName, 39 | AppUserDomainClazz: TestAppUser, 40 | appUserConnectionPropertyName: 'user', 41 | grailsApplication: grails 42 | ) 43 | TestAuthority._calls = [] 44 | TestRole._calls = [] 45 | TestFacebookUser._calls = [] 46 | TestAppUser._calls = [] 47 | securityConfig = [ 48 | userLookup: [ 49 | authorityJoinClassName: TestRole.canonicalName, 50 | usernamePropertyName: 'username', 51 | passwordPropertyName: 'password', 52 | enabledPropertyName: 'enabled', 53 | accountExpiredPropertyName: 'expired', 54 | accountLockedPropertyName: 'locked', 55 | passwordExpiredPropertyName: 'passwordExpired', 56 | ], 57 | authority: [ 58 | className: TestAuthority.canonicalName, 59 | nameField: 'name' 60 | ] 61 | ] 62 | SpringSecurityUtils.metaClass.static.getSecurityConfig = { 63 | return securityConfig 64 | } 65 | 66 | } 67 | 68 | def "Use service for findUser"() { 69 | setup: 70 | def args = [] 71 | TestFacebookUser user = new TestFacebookUser() 72 | def service = new Object() 73 | service.metaClass.findUser = { long uid -> 74 | args << uid 75 | return user 76 | } 77 | DefaultFacebookAuthDao dao = new DefaultFacebookAuthDao() 78 | dao.facebookAuthService = service 79 | when: 80 | def act = dao.findUser(1) 81 | then: 82 | act == user 83 | args == [1] 84 | } 85 | 86 | def "Find user by uid, when not exist"() { 87 | setup: 88 | DefaultFacebookAuthDao dao = new DefaultFacebookAuthDao( 89 | FacebookUserDomainClazz: TestFacebookUser, 90 | AppUserDomainClazz: TestAppUser 91 | ) 92 | List args = [] 93 | TestFacebookUser.metaClass.static.findWhere = { Map x -> 94 | args << x 95 | return null 96 | } 97 | when: 98 | def act = dao.findUser(1) 99 | then: 100 | act == null 101 | args[0] == [uid: 1] 102 | } 103 | 104 | def "Find user by uid, when exist"() { 105 | setup: 106 | DefaultFacebookAuthDao dao = new DefaultFacebookAuthDao( 107 | FacebookUserDomainClazz: TestFacebookUser, 108 | AppUserDomainClazz: TestAppUser, 109 | appUserConnectionPropertyName: 'user' 110 | ) 111 | List args = [] 112 | TestAppUser appUser = new TestAppUser() 113 | TestFacebookUser user = new TestFacebookUser(uid: 1, user: appUser) 114 | TestFacebookUser.metaClass.static.findWhere = { Map x -> 115 | args << x 116 | return user 117 | } 118 | when: 119 | def act = dao.findUser(1) 120 | then: 121 | act == user 122 | args[0] == [uid: 1] 123 | } 124 | 125 | def "Use service for create"() { 126 | setup: 127 | def args = [] 128 | TestFacebookUser user = new TestFacebookUser() 129 | def service = new Object() 130 | service.metaClass.create = { FacebookAuthToken token -> 131 | args << token 132 | return user 133 | } 134 | DefaultFacebookAuthDao dao = new DefaultFacebookAuthDao() 135 | dao.facebookAuthService = service 136 | FacebookAuthToken token = new FacebookAuthToken(uid: 1) 137 | when: 138 | def act = dao.create(token) 139 | then: 140 | act == user 141 | args == [token] 142 | } 143 | 144 | def "Basic create"() { 145 | setup: 146 | FacebookAuthToken token = new FacebookAuthToken(uid: 1, accessToken: new FacebookAccessToken( 147 | accessToken: 'test', 148 | expireAt: new Date(1000) 149 | )) 150 | when: 151 | def act = dao.create(token) 152 | then: 153 | act != null 154 | TestAuthority._calls == [ 155 | ['findByName', 'ROLE_USER'], 156 | ['findByName', 'ROLE_FACEBOOK'] 157 | ] 158 | TestRole._calls.size() == 1 159 | TestRole._calls[0][0] == 'create' 160 | TestAppUser._calls == [ 161 | ['save', [failOnError: true]] 162 | ] 163 | TestFacebookUser._calls == [ 164 | ['save', [failOnError: true]] 165 | ] 166 | act instanceof TestFacebookUser 167 | when: 168 | TestFacebookUser user = act 169 | then: 170 | user.uid == 1 171 | user.accessToken == 'test' 172 | user.accessTokenExpires == new Date(1000) 173 | user.user != null 174 | when: 175 | TestAppUser appUser = user.user 176 | then: 177 | appUser.enabled 178 | !appUser.expired 179 | !appUser.locked 180 | appUser.password != null 181 | !appUser.passwordExpired 182 | appUser.username == 'facebook_1' 183 | } 184 | 185 | def "Call notification methods on create"() { 186 | setup: 187 | List calls = [] 188 | FacebookAuthToken token = new FacebookAuthToken(uid: 1) 189 | def service = new Object() 190 | service.metaClass.onCreate = { TestFacebookUser a1, FacebookAuthToken a2 -> 191 | calls << ['onCreate', [a1, a2]] 192 | } 193 | service.metaClass.afterCreate = { TestFacebookUser a1, FacebookAuthToken a2 -> 194 | calls << ['afterCreate', [a1, a2]] 195 | } 196 | dao.facebookAuthService = service 197 | when: 198 | def act = dao.create(token) 199 | then: 200 | calls.size() == 2 201 | calls[0][0] == 'onCreate' 202 | calls[1][0] == 'afterCreate' 203 | } 204 | 205 | def "Use createRoles from service"() { 206 | setup: 207 | List calls = [] 208 | FacebookAuthToken token = new FacebookAuthToken(uid: 1) 209 | def service = new Object() 210 | service.metaClass.createRoles = { TestFacebookUser a1 -> 211 | calls << ['createRoles', a1] 212 | } 213 | dao.facebookAuthService = service 214 | when: 215 | def act = dao.create(token) 216 | then: 217 | calls.size() == 1 218 | calls[0][0] == 'createRoles' 219 | TestAuthority._calls == [] 220 | TestRole._calls == [] 221 | } 222 | 223 | def "Use createAppUser from service"() { 224 | setup: 225 | List calls = [] 226 | FacebookAuthToken token = new FacebookAuthToken(uid: 1) 227 | def service = new Object() 228 | TestAppUser appUser = new TestAppUser() 229 | service.metaClass.createAppUser = { TestFacebookUser a1, FacebookAuthToken a2 -> 230 | calls << ['createAppUser', a1, a2] 231 | return appUser 232 | } 233 | dao.facebookAuthService = service 234 | when: 235 | def act = dao.create(token) 236 | then: 237 | calls.size() == 1 238 | calls[0][0] == 'createAppUser' 239 | TestAppUser._calls == [] 240 | act.user == appUser 241 | } 242 | 243 | def "Use hasValidToken from service"() { 244 | setup: 245 | List calls = [] 246 | def service = new Object() 247 | TestFacebookUser testUser = new TestFacebookUser() 248 | service.metaClass.hasValidToken = { TestFacebookUser a1 -> 249 | calls << ['hasValidToken', a1] 250 | return false 251 | } 252 | dao.facebookAuthService = service 253 | when: 254 | def act = dao.hasValidToken(testUser) 255 | then: 256 | calls.size() == 1 257 | calls[0][0] == 'hasValidToken' 258 | act == false 259 | } 260 | 261 | def "Equal Dates"() { 262 | expect: 263 | dao.equalDates(x, y) 264 | where: 265 | x | y 266 | 100 | 100 267 | new Date() | new Date() 268 | new Date(5000) | new Date(5000) 269 | new Date(5000) | new Long(5000) 270 | new Date(5000) | 5000 271 | new Date(5000) | new Timestamp(5000) 272 | } 273 | 274 | def "Not Equal Dates"() { 275 | expect: 276 | !dao.equalDates(x, y) 277 | where: 278 | x | y 279 | 100 | null 280 | new Date() | new Date(516161) 281 | new Date(5000) | new Date(500061) 282 | new Date(5000) | new Long(5000948378) 283 | new Date(5000) | "hi!" 284 | new Date(5000) | new Timestamp(0) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/SpringSecurityFacebookGrailsPlugin.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import grails.plugins.Plugin 4 | import grails.util.Environment 5 | import grails.util.Metadata 6 | import org.slf4j.LoggerFactory 7 | import grails.plugin.springsecurity.SpringSecurityUtils 8 | 9 | class SpringSecurityFacebookGrailsPlugin extends Plugin { 10 | 11 | def grailsVersion = "3.0.10 > *" 12 | def pluginExcludes = [ 13 | "grails-app/views/error.gsp" 14 | ] 15 | 16 | def title = "Grails Spring Security Facebook" 17 | def author = 'Igor Artamonov' 18 | def authorEmail = 'igor@artamonov.ru' 19 | def description = 'Facebook Authentication for Spring Security Core plugin' 20 | 21 | def profiles = ['web'] 22 | 23 | def documentation = "http://splix.github.io/grails-spring-security-facebook/" 24 | 25 | def license = "APACHE" 26 | 27 | def organization = [ name: "The 6 Hours", url: "http://the6hours.com/" ] 28 | 29 | def issueManagement = [system: "GitHub", url: "https://github.com/splix/grails-spring-security-facebook/issues"] 30 | def scm = [url: "http://github.com/splix/grails-spring-security-facebook"] 31 | 32 | Closure doWithSpring() { {-> 33 | if (Environment.current == Environment.TEST) { 34 | if (Metadata.getCurrent().getApplicationName() == 'spring-security-facebook') { 35 | println "Test mode. Skipping initial plugin initialization" 36 | return 37 | } 38 | log.debug("Run in test mode") 39 | } 40 | 41 | def conf = SpringSecurityUtils.securityConfig 42 | if (!conf) { 43 | println 'ERROR: There is no Spring Security configuration' 44 | println 'ERROR: Stop configuring Spring Security Facebook' 45 | return 46 | } 47 | 48 | if (!hasProperty('log')) { 49 | println 'WARN: No such property: log for class: SpringSecurityFacebookGrailsPlugin' 50 | println 'WARN: Running from a unit test?' 51 | println 'WARN: Introducing a log property for plugin' 52 | this.metaClass.log = LoggerFactory.getLogger(SpringSecurityFacebookGrailsPlugin) 53 | } 54 | 55 | println 'Configuring Spring Security Facebook ...' 56 | SpringSecurityUtils.loadSecondaryConfig 'DefaultFacebookSecurityConfig' 57 | // have to get again after overlaying DefaultFacebookecurityConfig 58 | conf = SpringSecurityUtils.securityConfig 59 | 60 | def copy = conf.facebook.clone() 61 | ['appId', 'secret', 'apiKey'].each { if (copy[it] != 'Invalid') copy[it] = '********' } 62 | log.debug "Facebook security config: $copy" 63 | 64 | def _facebookDaoName = conf.facebook.bean.dao ?: null 65 | if (_facebookDaoName == null) { 66 | _facebookDaoName = 'facebookAuthDao' 67 | String _appUserConnectionPropertyName = getConfigValue(conf, 'facebook.domain.appUserConnectionPropertyName', 'facebook.domain.connectionPropertyName') 68 | List _roles = getAsStringList(conf.facebook.autoCreate.roles, 'grails.plugin.springsecurity.facebook.autoCreate.roles') 69 | facebookAuthDao(DefaultFacebookAuthDao) { 70 | domainClassName = conf.facebook.domain.classname 71 | appUserConnectionPropertyName = _appUserConnectionPropertyName 72 | userDomainClassName = conf.userLookup.userDomainClassName 73 | rolesPropertyName = conf.userLookup.authoritiesPropertyName 74 | coreUserDetailsService = ref('userDetailsService') 75 | defaultRoleNames = _roles 76 | } 77 | log.debug "Using default Facebook Auth DAO bean (DefaultFacebookAuthDao) with app user connection property name '$_appUserConnectionPropertyName' and default roles ${_roles}" 78 | } else { 79 | log.info("Using provided Facebook Auth DAO bean: $_facebookDaoName") 80 | } 81 | 82 | List _filterTypes = parseFilterTypes(conf) 83 | List _requiredPermissions = getAsStringList(conf.facebook.permissions, 'Required Permissions') 84 | 85 | facebookAuthUtils(FacebookAuthUtils) { 86 | apiKey = conf.facebook.apiKey 87 | secret = conf.facebook.secret 88 | applicationId = conf.facebook.appId 89 | apiVersion = conf.facebook.apiVersion ?: '' //Used unversioned Facebook API by default (for backwards compatibility) 90 | filterTypes = _filterTypes 91 | requiredPermissions = _requiredPermissions 92 | } 93 | log.debug "facebookAuthUtils filter types $_filterTypes and requiredPermissions $_requiredPermissions" 94 | 95 | boolean _createNew = getConfigValue(conf, 'facebook.autoCreate.enabled') ? conf.facebook.autoCreate.enabled as Boolean : false 96 | facebookAuthProvider(FacebookAuthProvider) { 97 | facebookAuthDao = ref(_facebookDaoName) 98 | facebookAuthUtils = ref('facebookAuthUtils') 99 | postAuthenticationChecks = ref('postAuthenticationChecks') 100 | createNew = _createNew 101 | } 102 | log.debug "registered facebookAuthProvider as an AuthenticationProvider; createNew: $_createNew" 103 | 104 | addFilters(conf, delegate, _filterTypes) 105 | println '... finished configuring Spring Security Facebook' 106 | }} 107 | 108 | void doWithApplicationContext() { 109 | SpringSecurityUtils.registerProvider 'facebookAuthProvider' 110 | } 111 | 112 | private List parseFilterTypes(conf) { 113 | List types 114 | 115 | def typesRaw = conf.facebook.filter.types 116 | if (!typesRaw) { 117 | typesRaw = conf.facebook.filter.type 118 | if (!typesRaw) { 119 | log.warn("Config options 'grails.plugin.springsecurity.facebook.filter.types' or 'grails.plugin.springsecurity.facebook.filter.type' are empty") 120 | } 121 | } 122 | 123 | String defaultType = 'transparent' 124 | List validTypes = ['transparent', 'cookieDirect', 'redirect', 'json'] 125 | 126 | if (!typesRaw) { 127 | log.error("Invalid Facebook Authentication filters configuration: '$typesRaw'. Should be used on of: $validTypes. Current value will be ignored, and type '$defaultType' will be used instead.") 128 | types = [defaultType] 129 | } else if (typesRaw instanceof Collection) { 130 | types = typesRaw*.toString().findAll { it in validTypes } 131 | } else if (typesRaw instanceof CharSequence) { 132 | types = typesRaw.toString().split(',').collect { it.trim() }.findAll { it in validTypes } 133 | } else { 134 | log.error("Invalid Facebook Authentication filters configuration, invalid value type: '${typesRaw.getClass()}'. Filter typer should be defined as a Collection or String (comma separated, if you need few filters). Type '$defaultType' will be used instead.") 135 | types = [defaultType] 136 | } 137 | 138 | if (!types) { 139 | log.error("Facebook Authentication filter is not configured. Should be used one of: $validTypes. So '$defaultType' will be used by default.") 140 | log.error("To configure Facebook Authentication filters you should add to application.yml:") 141 | log.error("grails.plugin.springsecurity.facebook.filter.types='transparent'") 142 | log.error("or") 143 | log.error("grails.plugin.springsecurity.facebook.filter.types='redirect,transparent,cookieDirect'") 144 | 145 | types = [defaultType] 146 | } 147 | return types 148 | } 149 | 150 | private void addFilters(conf, delegate, types) { 151 | int basePosition = conf.facebook.filter.position 152 | 153 | log.debug "SpringSecurityUtils.orderedFilters before registering this plugin's: $SpringSecurityUtils.orderedFilters" 154 | 155 | addFilter.delegate = delegate 156 | types.eachWithIndex { name, idx -> 157 | addFilter(conf, name, basePosition + 1 + idx) 158 | } 159 | 160 | log.debug "SpringSecurityUtils.orderedFilters after registering this plugin's: $SpringSecurityUtils.orderedFilters" 161 | } 162 | 163 | private addFilter = { conf, String name, int position -> 164 | if (name == 'transparent') { 165 | SpringSecurityUtils.registerFilter 'facebookAuthCookieTransparentFilter', position 166 | facebookAuthCookieTransparentFilter(FacebookAuthCookieTransparentFilter) { 167 | authenticationManager = ref('authenticationManager') 168 | facebookAuthUtils = ref('facebookAuthUtils') 169 | logoutUrl = conf.logout.filterProcessesUrl 170 | forceLoginParameter = conf.facebook.filter.forceLoginParameter 171 | } 172 | log.debug "registerFilter 'facebookAuthCookieTransparentFilter' at position $position; logoutUrl '$conf.logout.filterProcessesUrl', forceLoginParameter '$conf.facebook.filter.forceLoginParameter'" 173 | facebookAuthCookieLogout(FacebookAuthCookieLogoutHandler) { 174 | facebookAuthUtils = ref('facebookAuthUtils') 175 | facebookAuthDao = ref(_facebookDaoName) 176 | } 177 | SpringSecurityUtils.registerLogoutHandler('facebookAuthCookieLogout') 178 | log.debug "registerLogoutHandler 'facebookAuthCookieLogout'" 179 | } else if (name == 'cookieDirect') { 180 | String _successHandler = getConfigValue(conf, 'facebook.filter.cookieDirect.successHandler') 181 | String _failureHandler = getConfigValue(conf, 'facebook.filter.cookieDirect.failureHandler') 182 | String url = getConfigValue(conf, 'facebook.filter.cookieDirect.processUrl', 'facebook.filter.processUrl') 183 | SpringSecurityUtils.registerFilter 'facebookAuthCookieDirectFilter', position 184 | log.debug "registerFilter 'facebookAuthCookieDirectFilter' at position $position; logoutUrl '$conf.logout.filterProcessesUrl', forceLoginParameter '$conf.facebook.filter.forceLoginParameter'" 185 | facebookAuthCookieDirectFilter(FacebookAuthCookieDirectFilter, url) { 186 | authenticationManager = ref('authenticationManager') 187 | rememberMeServices = ref('rememberMeServices') 188 | facebookAuthUtils = ref('facebookAuthUtils') 189 | if (_successHandler) { 190 | authenticationSuccessHandler = ref(_successHandler) 191 | } 192 | if (_failureHandler) { 193 | authenticationFailureHandler = ref(_failureHandler) 194 | } 195 | } 196 | } else if (name == 'redirect') { 197 | SpringSecurityUtils.registerFilter 'facebookAuthRedirectFilter', position 198 | String successHandler = getConfigValue(conf, 'facebook.filter.redirect.successHandler') 199 | String failureHandler = getConfigValue(conf, 'facebook.filter.redirect.failureHandler') 200 | String _url = getConfigValue(conf, 'facebook.filter.redirect.processUrl', 'facebook.filter.processUrl') 201 | String _redirectFromUrl = getConfigValue(conf, 'facebook.filter.redirect.redirectFromUrl', 'facebook.filter.redirectFromUrl') 202 | facebookAuthRedirectFilter(FacebookAuthRedirectFilter, _url) { 203 | authenticationManager = ref('authenticationManager') 204 | rememberMeServices = ref('rememberMeServices') 205 | facebookAuthUtils = ref('facebookAuthUtils') 206 | redirectFromUrl = _redirectFromUrl 207 | linkGenerator = ref('grailsLinkGenerator') 208 | if (successHandler) { 209 | authenticationSuccessHandler = ref(successHandler) 210 | } 211 | if (failureHandler) { 212 | authenticationFailureHandler = ref(failureHandler) 213 | } 214 | } 215 | log.debug "registerFilter 'facebookAuthRedirectFilter' at position $position; _redirectFromUrl '$_redirectFromUrl', processUrl '$_url'" 216 | } else if (name == 'json') { 217 | SpringSecurityUtils.registerFilter 'facebookAuthJsonFilter', position 218 | String _url = conf.facebook.filter.json.processUrl 219 | boolean _jsonp = '_jsonp'.equalsIgnoreCase(conf.facebook.filter.json.type) 220 | facebookJsonAuthenticationHandler(JsonAuthenticationHandler) { 221 | useJsonp = _jsonp 222 | } 223 | List _methods 224 | if (_jsonp) { 225 | _methods = ['GET'] 226 | } 227 | else { 228 | _methods = getAsStringList(conf.facebook.filter.json.methods, '**.facebook.filter.json.type') 229 | _methods = _methods ? _methods*.toUpperCase() : ['POST'] 230 | } 231 | facebookAuthJsonFilter(FacebookAuthJsonFilter, _url) { 232 | methods = _methods 233 | authenticationManager = ref('authenticationManager') 234 | facebookAuthUtils = ref('facebookAuthUtils') 235 | authenticationSuccessHandler = ref('facebookJsonAuthenticationHandler') 236 | authenticationFailureHandler = ref('facebookJsonAuthenticationHandler') 237 | } 238 | log.debug "registerFilter 'facebookAuthJsonFilter' at position $position; useJsonp '$_jsonp', processUrl '$_url', methods: $_methods" 239 | } else { 240 | log.error("Invalid filter type: $name") 241 | } 242 | } 243 | 244 | private getConfigValue(conf, String... values) { 245 | def flatConf = conf.flatten() 246 | String key = values.find { 247 | if (!flatConf.containsKey(it)) { 248 | return false 249 | } 250 | def val = flatConf.get(it) 251 | if (val == null || (val instanceof ConfigObject && val.isEmpty())) { 252 | return false 253 | } 254 | return true 255 | } 256 | key ? flatConf[key] : null 257 | } 258 | 259 | private List getAsStringList(conf, String paramHumanName) { 260 | if (conf == null) { 261 | log.error("Invalid $paramHumanName filters configuration: '$conf'") 262 | return null 263 | } 264 | if (conf instanceof Collection) { 265 | return conf*.toString() 266 | } 267 | if (conf instanceof CharSequence) { 268 | return conf.toString().split(',').collect { it.trim() } 269 | } 270 | log.error("Invalid $paramHumanName filters configuration, invalid value type: '${conf.getClass()}'. Value should be defined as a Collection or String (comma separated)") 271 | return null 272 | } 273 | 274 | } 275 | -------------------------------------------------------------------------------- /src/main/groovy/com/the6hours/grails/springsecurity/facebook/DefaultFacebookAuthDao.groovy: -------------------------------------------------------------------------------- 1 | package com.the6hours.grails.springsecurity.facebook 2 | 3 | import grails.core.GrailsApplication 4 | import grails.core.support.GrailsApplicationAware 5 | import grails.plugin.springsecurity.SpringSecurityUtils 6 | import grails.plugin.springsecurity.userdetails.GormUserDetailsService 7 | 8 | import java.util.concurrent.TimeUnit 9 | 10 | import org.slf4j.Logger 11 | import org.slf4j.LoggerFactory 12 | import org.springframework.beans.factory.InitializingBean 13 | import org.springframework.context.ApplicationContext 14 | import org.springframework.context.ApplicationContextAware 15 | import org.springframework.dao.OptimisticLockingFailureException 16 | import org.springframework.security.core.GrantedAuthority 17 | import org.springframework.security.core.authority.SimpleGrantedAuthority 18 | import org.springframework.security.core.userdetails.UserDetails 19 | 20 | /** 21 | * Default Facebook Authentication Dao 22 | * Covers most cases, and custom logic could be added with custom FacebookAuthService 23 | * 24 | * @since 28.10.11 25 | * @author Igor Artamonov (http://igorartamonov.com) 26 | */ 27 | class DefaultFacebookAuthDao implements FacebookAuthDao, InitializingBean, ApplicationContextAware, GrailsApplicationAware { 28 | 29 | private static final Logger log = LoggerFactory.getLogger(this) 30 | 31 | List defaultRoleNames = ['ROLE_USER', 'ROLE_FACEBOOK'] 32 | 33 | GrailsApplication grailsApplication 34 | ApplicationContext applicationContext 35 | def coreUserDetailsService 36 | def facebookAuthService 37 | 38 | String appUserConnectionPropertyName = 'user' 39 | String rolesPropertyName 40 | 41 | @Deprecated String domainClassName 42 | @Deprecated String userDomainClassName 43 | @Deprecated DomainsRelation domainsRelation 44 | 45 | private Class FacebookUserDomainClazz 46 | private Class AppUserDomainClazz 47 | 48 | boolean isSameDomain() { 49 | FacebookUserDomainClazz == AppUserDomainClazz 50 | } 51 | 52 | def getFacebookUser(appUser) { 53 | if (appUser && facebookAuthService?.respondsTo('getFacebookUser', appUser.class)) { 54 | return facebookAuthService.getFacebookUser(appUser) 55 | } 56 | if (isSameDomain()) { 57 | return appUser 58 | } 59 | FacebookUserDomainClazz.withTransaction { status -> 60 | FacebookUserDomainClazz.findWhere((appUserConnectionPropertyName): appUser) 61 | } 62 | } 63 | 64 | def getAppUser(facebookUser) { 65 | if (!facebookUser) { 66 | log.warn("Passed facebookUser is null") 67 | return facebookUser 68 | } 69 | 70 | if (facebookAuthService?.respondsTo('getAppUser', facebookUser.class)) { 71 | return facebookAuthService.getAppUser(facebookUser) 72 | } 73 | 74 | if (isSameDomain()) { 75 | return facebookUser 76 | } 77 | 78 | FacebookUserDomainClazz.withTransaction { status -> 79 | facebookUser.getProperty(appUserConnectionPropertyName) 80 | } 81 | } 82 | 83 | def findUser(long uid) { 84 | if (facebookAuthService?.respondsTo('findUser', Long)) { 85 | return facebookAuthService.findUser(uid) 86 | } 87 | 88 | def user 89 | FacebookUserDomainClazz.withTransaction { status -> 90 | user = FacebookUserDomainClazz.findWhere(uid: uid) 91 | if (!user) { 92 | return user 93 | } 94 | 95 | if (!isSameDomain()) { 96 | if (appUserConnectionPropertyName) { 97 | // load the User object to memory prevent LazyInitializationException 98 | def appUser = user.getProperty(appUserConnectionPropertyName) 99 | if (!appUser) { 100 | log.warn("No appUser for facebookUser ${user}. Property ${appUserConnectionPropertyName} have null value") 101 | } 102 | } else { 103 | log.error("appUserConnectionPropertyName is not configured") 104 | } 105 | } 106 | } 107 | return user 108 | } 109 | 110 | def create(FacebookAuthToken token) { 111 | if (facebookAuthService?.respondsTo('create', FacebookAuthToken)) { 112 | return facebookAuthService.create(token) 113 | } 114 | 115 | def securityConf = SpringSecurityUtils.securityConfig 116 | 117 | def user = grailsApplication.getArtefact('Domain', FacebookUserDomainClazz.canonicalName).newInstance() 118 | user.setProperty('uid', token.uid) 119 | if (user.hasProperty('accessToken')) { 120 | user.setProperty('accessToken', token.accessToken?.accessToken) 121 | } 122 | if (user.hasProperty('accessTokenExpires')) { 123 | user.setProperty('accessTokenExpires', token.accessToken?.expireAt) 124 | } 125 | 126 | def appUser 127 | if (!isSameDomain()) { 128 | if (facebookAuthService?.respondsTo('createAppUser', FacebookUserDomainClazz, FacebookAuthToken)) { 129 | appUser = facebookAuthService.createAppUser(user, token) 130 | } else { 131 | appUser = grailsApplication.getArtefact('Domain', AppUserDomainClazz.canonicalName).newInstance() 132 | if (facebookAuthService?.respondsTo('prepopulateAppUser', AppUserDomainClazz, FacebookAuthToken)) { 133 | facebookAuthService.prepopulateAppUser(appUser, token) 134 | } else { 135 | def ul = securityConf.userLookup 136 | appUser.setProperty(ul.usernamePropertyName, "facebook_$token.uid") 137 | appUser.setProperty(ul.passwordPropertyName, token.accessToken?.accessToken) 138 | appUser.setProperty(ul.enabledPropertyName, true) 139 | appUser.setProperty(ul.accountExpiredPropertyName, false) 140 | appUser.setProperty(ul.accountLockedPropertyName, false) 141 | appUser.setProperty(ul.passwordExpiredPropertyName, false) 142 | } 143 | AppUserDomainClazz.withTransaction { 144 | appUser.save(failOnError: true) 145 | } 146 | } 147 | user[appUserConnectionPropertyName] = appUser 148 | } else { 149 | appUser = user 150 | } 151 | 152 | if (facebookAuthService?.respondsTo('onCreate', FacebookUserDomainClazz, FacebookAuthToken)) { 153 | facebookAuthService.onCreate(user, token) 154 | } 155 | 156 | FacebookUserDomainClazz.withTransaction { 157 | user.save(failOnError: true) 158 | } 159 | 160 | if (facebookAuthService?.respondsTo('afterCreate', FacebookUserDomainClazz, FacebookAuthToken)) { 161 | facebookAuthService.afterCreate(user, token) 162 | } 163 | 164 | if (facebookAuthService?.respondsTo('createRoles', FacebookUserDomainClazz)) { 165 | facebookAuthService.createRoles(user) 166 | } else { 167 | Class PersonRole = grailsApplication.getArtefact('Domain', securityConf.userLookup.authorityJoinClassName)?.clazz 168 | Class Authority = grailsApplication.getArtefact('Domain', securityConf.authority.className)?.clazz 169 | String authorityNameField = securityConf.authority.nameField 170 | String findByField = authorityNameField[0].toUpperCase() + authorityNameField.substring(1) 171 | log.debug("Add roles: $defaultRoleNames") 172 | PersonRole.withTransaction { status -> 173 | defaultRoleNames.each { String roleName -> 174 | def auth = Authority."findBy${findByField}"(roleName) 175 | if (auth) { 176 | PersonRole.create(appUser, auth) 177 | } else { 178 | log.error("Can't find authority for name '$roleName'") 179 | } 180 | } 181 | } 182 | } 183 | 184 | return user 185 | } 186 | 187 | def getPrincipal(user) { 188 | if (user && facebookAuthService?.respondsTo('getPrincipal', user.class)) { 189 | return facebookAuthService.getPrincipal(user) 190 | } 191 | if (coreUserDetailsService) { 192 | return coreUserDetailsService.createUserDetails(user, getRoles(user)) 193 | } 194 | return user 195 | } 196 | 197 | Collection getRoles(user) { 198 | if (!user) { 199 | return [] 200 | } 201 | 202 | if (facebookAuthService?.respondsTo('getRoles', user.class)) { 203 | return facebookAuthService.getRoles(user) 204 | } 205 | 206 | if (user instanceof UserDetails) { 207 | return user.authorities 208 | } 209 | 210 | def conf = SpringSecurityUtils.securityConfig 211 | Class PersonRole = grailsApplication.getArtefact('Domain', conf.userLookup.authorityJoinClassName)?.clazz 212 | if (!PersonRole) { 213 | log.error("Can't load roles for user $user. Reason: can't find ${conf.userLookup.authorityJoinClassName} class") 214 | return [] 215 | } 216 | 217 | Collection roles 218 | PersonRole.withTransaction { status -> 219 | roles = user?.getProperty(rolesPropertyName) 220 | } 221 | if (!roles) { 222 | roles = [] 223 | } 224 | if (roles.empty) { 225 | return roles 226 | } 227 | 228 | String nameField = conf.authority.nameField 229 | return roles.collect { 230 | new SimpleGrantedAuthority(it instanceof CharSequence ? it.toString() : it.getProperty(nameField)) 231 | } 232 | } 233 | 234 | Boolean hasValidToken(facebookUser) { 235 | if (facebookUser && facebookAuthService?.respondsTo('hasValidToken', facebookUser.class)) { 236 | return facebookAuthService.hasValidToken(facebookUser) 237 | } 238 | if (facebookUser.hasProperty('accessToken')) { 239 | if (!facebookUser.getProperty('accessToken')) { 240 | return false 241 | } 242 | } 243 | if (facebookUser.hasProperty('accessTokenExpires')) { 244 | if (!facebookUser.getProperty('accessTokenExpires')) { 245 | return false 246 | } 247 | Date goodExpiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(15)) 248 | Date currentExpires = facebookUser.getProperty('accessTokenExpires') 249 | if (currentExpires.before(goodExpiration)) { 250 | return false 251 | } 252 | } else { 253 | log.warn("Domain ${facebookUser.class} don't have 'acccessTokenExpires' field, can't check accessToken expiration. And it's very likely that your database contains expired tokens") 254 | } 255 | return true 256 | } 257 | 258 | void updateToken(facebookUser, FacebookAuthToken token) { 259 | if (facebookUser && facebookAuthService?.respondsTo('updateToken', facebookUser.class, FacebookAuthToken)) { 260 | facebookAuthService.updateToken(facebookUser, token) 261 | return 262 | } 263 | if (!token.accessToken) { 264 | log.error("No access token $token") 265 | return 266 | } 267 | if (!token.accessToken.accessToken) { 268 | log.warn("Update to empty accessToken for user $facebookUser") 269 | } 270 | log.debug("Update access token to $token.accessToken for $facebookUser") 271 | FacebookUserDomainClazz.withTransaction { 272 | try { 273 | boolean updated = false 274 | if (!facebookUser.isAttached()) { 275 | facebookUser.attach() 276 | } 277 | if (facebookUser.hasProperty('accessToken')) { 278 | if (facebookUser.getProperty('accessToken') != token.accessToken.accessToken) { 279 | updated = true 280 | facebookUser.setProperty('accessToken', token.accessToken.accessToken) 281 | } 282 | } 283 | if (updated && facebookUser.hasProperty('accessTokenExpires')) { 284 | if (!equalDates(facebookUser.getProperty('accessTokenExpires'), token.accessToken.expireAt)) { 285 | if (token.accessToken.expireAt || !token.accessToken.accessToken) { //allow null only if both expireAt and accessToken are null 286 | updated = true 287 | facebookUser.setProperty('accessTokenExpires', token.accessToken.expireAt) 288 | } else { 289 | log.warn("Provided accessToken.expiresAt value is null. Skip update") 290 | } 291 | } else { 292 | log.warn("A new accessToken have same token but different expires: $token") 293 | } 294 | } 295 | if (updated) { 296 | facebookUser.save() 297 | } 298 | } catch (OptimisticLockingFailureException e) { 299 | log.warn("Seems that token was updated in another thread (${e.message}). Skip") 300 | } catch (Throwable e) { 301 | log.error("Can't update token", e) 302 | } 303 | } 304 | } 305 | 306 | boolean equalDates(x, y) { 307 | long xtime = dateToLong(x) 308 | long ytime = dateToLong(y) 309 | return xtime >= 0 && ytime >= 0 && Math.abs(xtime - ytime) < 1000 //for dates w/o millisecond 310 | } 311 | 312 | long dateToLong(date) { 313 | if (date == null) { 314 | return -1 315 | } 316 | if (date instanceof Date) { //java.sql.Timestamp extends Date 317 | return date.time 318 | } 319 | if (date instanceof Number) { 320 | return date.toLong() 321 | } 322 | log.warn("Cannot convert date: $date (class: ${date.class.name})") 323 | return -1 324 | } 325 | 326 | String getAccessToken(facebookUser) { 327 | if (facebookUser && facebookAuthService?.respondsTo('getAccessToken', facebookUser.class)) { 328 | return facebookAuthService.getAccessToken(facebookUser) 329 | } 330 | if (facebookUser.hasProperty('accessToken')) { 331 | if (facebookUser.hasProperty('accessTokenExpires')) { 332 | Date currentExpires = facebookUser.getProperty('accessTokenExpires') 333 | if (currentExpires == null) { 334 | log.debug("Current access token don't have expiration timeout, and should be updated") 335 | return null 336 | } 337 | if (currentExpires.before(new Date())) { 338 | log.debug("Current access token is expired, and cannot be used anymore") 339 | return null 340 | } 341 | } 342 | return facebookUser.getProperty('accessToken') 343 | } 344 | return null 345 | } 346 | 347 | void afterPropertiesSet() { 348 | if (!facebookAuthService) { 349 | if (applicationContext.containsBean('facebookAuthService')) { 350 | log.debug("Use provided facebookAuthService") 351 | facebookAuthService = applicationContext.facebookAuthService 352 | } 353 | } 354 | 355 | //validate configuration 356 | 357 | List serviceMethods = facebookAuthService ? facebookAuthService.metaClass.methods*.name : [] 358 | 359 | def conf = SpringSecurityUtils.securityConfig 360 | if (!serviceMethods.contains('getRoles')) { 361 | if (!userDomainClassName) { 362 | log.error("User domain class name is not configured") 363 | } else { 364 | Class UserDomainClass = grailsApplication.getArtefact('Domain', userDomainClassName)?.clazz 365 | if (!UserDomainClass || !UserDetails.isAssignableFrom(UserDomainClass)) { 366 | if (!conf.userLookup.authorityJoinClassName) { 367 | log.error("Don't have authority join class configuration. Please configure 'grails.plugin.springsecurity.userLookup.authorityJoinClassName' value") 368 | } else if (!grailsApplication.getArtefact('Domain', conf.userLookup.authorityJoinClassName)) { 369 | log.error("Can't find authority join class (${conf.userLookup.authorityJoinClassName}). Please configure 'grails.plugin.springsecurity.userLookup.authorityJoinClassName' value, or create your own 'List facebookAuthService.getRoles(user)'") 370 | } 371 | } 372 | } 373 | } 374 | if (!serviceMethods.contains('findUser')) { 375 | if (!domainClassName) { 376 | log.error("Don't have facebook user class configuration. Please configure 'grails.plugin.springsecurity.facebook.domain.classname' value") 377 | } else { 378 | Class User = grailsApplication.getArtefact('Domain', domainClassName)?.clazz 379 | if (!User) { 380 | log.error("Can't find facebook user class ($domainClassName). Please configure 'grails.plugin.springsecurity.facebook.domain.classname' value, or create your own 'Object facebookAuthService.findUser(long)'") 381 | } 382 | } 383 | } 384 | 385 | if (coreUserDetailsService) { 386 | if (!(coreUserDetailsService.respondsTo('createUserDetails'))) { 387 | log.error("UserDetailsService from spring-security-core don't have method 'createUserDetails()'") 388 | coreUserDetailsService = null 389 | } else if (!(coreUserDetailsService instanceof GormUserDetailsService)) { 390 | log.warn("UserDetailsService from spring-security-core isn't instance of GormUserDetailsService, but: ${coreUserDetailsService.class}") 391 | } 392 | } else { 393 | log.warn("No UserDetailsService bean from spring-security-core") 394 | } 395 | 396 | if (domainClassName && !FacebookUserDomainClazz) { 397 | FacebookUserDomainClazz = grailsApplication.getArtefact('Domain', domainClassName)?.clazz 398 | } 399 | if (!FacebookUserDomainClazz) { 400 | log.error("Can't find domain: $domainClassName") 401 | } 402 | if (userDomainClassName && !AppUserDomainClazz) { 403 | AppUserDomainClazz = grailsApplication.getArtefact('Domain', userDomainClassName)?.clazz 404 | } 405 | if (!AppUserDomainClazz) { 406 | log.error("Can't find domain: $userDomainClassName") 407 | } 408 | if (FacebookUserDomainClazz && AppUserDomainClazz) { 409 | if (FacebookUserDomainClazz == AppUserDomainClazz) { 410 | domainsRelation = DomainsRelation.SameObject 411 | } 412 | } 413 | if (domainsRelation == null) { 414 | domainsRelation = DomainsRelation.JoinedUser 415 | } 416 | } 417 | } 418 | --------------------------------------------------------------------------------