Created
September 25, 2017 11:17
-
-
Save OsaSoft/d805672062deb0da277267ca4d86b335 to your computer and use it in GitHub Desktop.
Grails 3 Spring Security Rest - Native Facebook mobile login
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Seeing that I couldn't really find much information on how to use Facebook's native mobile login working with a secured Grails | |
REST API, I decided to post my solution in the form of this gist. Hope it helps! | |
The problem: Spring Security Rest plugin supports Facebook login only using oAuth, which is to be used with a JS frontend and uses | |
redirects and such. However, when developing a mobile app, you can use Facebook's native mobile login, which creates a Facebook Access Token. | |
This gist describes my solution for creating a custom authentication flow using Facebook's Access Token in order to authenticate | |
a user against a Grails 3 REST API secured with Spring Security and Spring Security Rest plugins. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class FacebookController { | |
def authenticationEventPublisher | |
def userDetailsService | |
def tokenGenerator | |
def tokenStorageService | |
def accessTokenJsonRenderer | |
def facebookService | |
//... | |
def auth() { | |
def fbToken = request.JSON?.fb_access_token ?: request['fb_access_token'] //get fb token from params or json payload | |
if (!facebookService.isTokenForApp(fbToken)) { //verify if token is for our app | |
respond([error: "Incorrect app access token"], status: HttpStatus.BAD_REQUEST) | |
return | |
} | |
def userInfo = facebookService.getUserInfo(fbToken) //get user info from facebook | |
if (!userInfo?.id) { //something went wrong | |
respond([error: userInfo?.error], status: HttpStatus.UNAUTHORIZED) | |
return | |
} | |
userInfo = new FacebookUserInfo( //or whatever info it is you request and need | |
id: userInfo.id, | |
email: userInfo.email, | |
firstname: userInfo.first_name, | |
lastname: userInfo.last_name, | |
profilePictureUrl: userInfo.picture?.data?.url | |
) | |
User user = User.facebookLogin(userInfo) | |
if (!user || user.hasErrors()) { | |
respond([error: [message: "persist error", errors: user.errors]], status: HttpStatus.INTERNAL_SERVER_ERROR) | |
return | |
} | |
def userDetails = userDetailsService.loadUserByUsername(user.username) | |
//create and store API access token, whatever your token generation and storage strategy may be, | |
//this will work (ie JWT, GORM, etc) | |
AccessToken accessToken = tokenGenerator.generateAccessToken(userDetails) | |
tokenStorageService.storeToken(accessToken.accessToken, userDetails) | |
authenticationEventPublisher.publishTokenCreation(accessToken) | |
SecurityContextHolder.context.setAuthentication(accessToken) | |
render contentType: 'application/json', encoding: 'UTF-8', text: accessTokenJsonRenderer.generateJson(accessToken) | |
} | |
//... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class FacebookService { | |
@Value('${facebook.graph.api.url}') | |
String fbApiUrl | |
@Value('${facebook.graph.api.key}') | |
String fbAppId | |
final USER_INFO = "me" | |
final APP = "app" | |
RESTClient client | |
@PostConstruct | |
void init() { | |
client = new RESTClient(fbApiUrl) | |
client.handler.failure = client.handler.success //so it doesnt throw Exceptions when status is !=200 | |
} | |
def getUserInfo(String fbAcessToken) { | |
def resp = client.get(path: USER_INFO, query: [access_token: fbAcessToken, fields: User.facebookFields]) | |
if (resp.status != 200) { | |
log.error "Error getting user information! $resp.status: $resp.data" | |
} else { | |
resp.data | |
} | |
} | |
boolean isTokenForApp(String fbAccessToken) { | |
def resp = client.get(path: APP, query: [access_token: fbAccessToken]) | |
resp.status == 200 && resp.data.id == fbAppId | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class UrlMappings { | |
static mappings = { | |
//... | |
//custom mappings | |
post "/facebook/auth(.$format)?"(controller: "facebook", action: "auth") | |
//... | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class User implements Serializable { | |
static final facebookFields = "id,email,first_name,last_name,picture" //or whatever fields you need | |
//... | |
static User facebookLogin(FacebookUserInfo fbInfo) { | |
def user = findByUsername(fbInfo.id) | |
boolean isNew = false | |
if (!user) { //does user already exist? | |
isNew = true | |
user = new User(username: fbInfo.id, password: RandomStringUtils.randomAlphanumeric(16)) | |
} | |
user.with { //or other fields you want | |
firstname = fbInfo.firstname | |
lastname = fbInfo.lastname | |
fbPictureUrl = fbInfo.profilePictureUrl | |
} | |
if (user.isDirty() || isNew) { //if any fields changed or is a new user, save | |
user.save(flush: true) | |
} | |
if (!user.hasErrors()) { //give user roles if user saved properly | |
def userRole = Role.findByAuthority("ROLE_USER") | |
def facebookRole = Role.findByAuthority("ROLE_FACEBOOK") | |
if (!user.authorities.contains(userRole)) UserRole.create(user, userRole, true) | |
if (!user.authorities.contains(facebookRole)) UserRole.create(user, facebookRole, true) | |
} | |
user | |
} | |
//... | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This saved me from a lot of headache. Thanks a lot for writing this!