Skip to content

Instantly share code, notes, and snippets.

@OsaSoft
Created September 25, 2017 11:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OsaSoft/d805672062deb0da277267ca4d86b335 to your computer and use it in GitHub Desktop.
Save OsaSoft/d805672062deb0da277267ca4d86b335 to your computer and use it in GitHub Desktop.
Grails 3 Spring Security Rest - Native Facebook mobile login
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.
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)
}
//...
}
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
}
}
class UrlMappings {
static mappings = {
//...
//custom mappings
post "/facebook/auth(.$format)?"(controller: "facebook", action: "auth")
//...
}
}
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
}
//...
}
@vahurh
Copy link

vahurh commented Jan 15, 2020

This saved me from a lot of headache. Thanks a lot for writing this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment