Monday 20 October 2008

Grails: Hacking JSecurity plugin to supports OpenId

I'm currently developing an application for my new brand company and I'd like that supports authentication with username and password , and OpenId.

I could install Acegi Grails Plugin, but I'm very happy using JSecurity, ok no problem let's hack.
First, I have to install OpenId Plugin to support OpenId authenticantion, with this plugin I can manage login process and get openid identifier for OpenId users.

With JSecurity installed and done QuickStart, I need to pass Openid identifier in auth process, for this I've created class OpenIdContextHolder to save in a ThreadLocal context.
class OpenIdContextHolder{

private static final ThreadLocal openIdContextHolder = new ThreadLocal();

static void resetContext() {
openIdContextHolder.set(null);
}

static def getOpenIdIdentifier(){
openIdContextHolder.get()
}

static void setOpenIdIdentifier(id){
openIdContextHolder.set(id)
}

}
Now I have to hack AuthController to manage openid issues. The trick is when a user try to signIn, if I recive an OpenId identifier from OpenId Plugin then I put it in OpenIdContextHolder.

def login = {
if(openidService.isLoggedIn(session)){
return redirect(action:'signIn')
}

return [ username: params.username, rememberMe: (params.rememberMe != null), targetUri: params.targetUri ]
}

def signIn = {
// if is logged with openid set contextholder
if(openidService.isLoggedIn(session)){
def openId = openidService.getIdentifier(session)
OpenIdContextHolder.setOpenIdIdentifier(openId)
params.rememberMe = true
params.username = openId
params.password = "nullpass"
}
def authToken = new UsernamePasswordToken(params.username, params.password)

// continues the default generated code...
// ...
}
Next step is update JsecDbRealm generated. I have to retrieve OpenId identifier from ContextHolder, and lookup in my domain objects. I use the trick to register my openId users in JScecurity with the password 'secret' but if anyone try to access with username and password in this kind of users, I throw an Exception.
def authenticate(authToken) {
log.info "Attempting to authenticate ${authToken.username} in DB realm..."

// experimental!!
def openid = OpenIdContextHolder.getOpenIdIdentifier()
OpenIdContextHolder.resetContext()
log.info "OpenIdContextHolder request with openid: ${openid}"
if(openid){
def openidUser = User.findByOpenid(openid)
if (!openidUser)
throw new UnknownAccountException("No account found for user [${username}]")
log.info "Jsecurity with Openid ${openidUser.username} : ${openidUser.openid}"
authToken.password = 'secret'
authToken.username = openidUser.username
}else {
def openidUser = User.findByUsername(authToken.username)
if(openidUser?.openid?.trim()){
// trying to access with password for openid user
log.info "Jsecurity: Trying to access with password for user: ${openidUser.username} : ${openidUser.openid}"
throw new IncorrectCredentialsException("Invalid password for openid user '${authToken.username}', try to use openid instead user:password")
}
}

def username = authToken.username
// continues the default generated code...
// ...
The last step is redirect, openid users to signIn controller after logged (you only have to change it in OpenId Plugin).

And that's all folks.