15 Sep 2015

Grails rest api using oauth2

Grails, REST 0 Comment

The intent is to implement OAuth2 provider protocol & secure appropriate rest api’s using OAuth2 token.

The generic need for any application would be to have some public API’s that can be accessed without any security, & some secured using token associated with clientId/secretKey & some secured using token associated with user credentials.

The following are the rest endpoints supporting OAuth2, which provide a token using different protocols, i.e Authorization Code Grant flow (1a,1b), Implicit Grant flow (2), Resource Owner Password Grant (3), Client Credentials Grant (4), Refresh Token Grant(5).

  • OAuth2 Rest End Points 
      1. [ GET] /oauth/authorize:
        • (1a) OAuth2 Authorization Code Grant
          parameters [ ?response_type=code&client_id=my-client&scope=read ]
        • (2) OAuth2 Implicit Grant
          parameters [ ?response_type=token&client_id=my-client&scope=read ]
      2. [ POST] /oauth/token:
            • (1b) OAuth2 Authorization Code Grant
              parameters [ ?grant_type=authorization_code&code=139R59&client_id=my-client ]
            • (3) OAuth2 Resource Owner Password Grant.
              parameters [ ?grant_type=password&client_id=..&client_secret=my-secret&username=aa&password=bb&scope=read ]
            • (4) OAuth2 Client Credentials Grant
              parameters [ ?grant_type=client_credentials&client_id=my-client&scope=read ]
            • (5) OAuth2 Refresh Token Grant
              parameters [ ?grant_type=refresh_token&refresh_token=22c3f395-690c-4838-a14f-3137e6802db1&client_id=my-client&scope=read ]

The following are the API endpoints we would like to support using the token provided above

    1. Public API’s
      1. [  GET] /api/v0/about
      2. [  GET] /api/v0/sample/about
    2. Secured access using a Client Token
      1. [  GET] /api/v0/sample/securedWithClientOAuth : Sample API secured with client token
      2. [POST] /api/v0/members/signup: Create a new User
      3. [POST] /api/v0/auth/login: Login using username/password credentials
    3. Secured access using User Token
      1. [  GET] /api/v0/sample/securedWithUserOAuth: Sample Secured with user token
      2. [  GET] /api/v0/members: Get list of members supports pagination ?filter=”field1=value,field2=v”&sort=”field2″&offset=15
      3. [  GET] /api/v0/members/{memberId}: Get information about a user
      4. [  PUT] /api/v0/members/{memberId}: update a user
      5. [  GET] /api/v0/auth/logout: logout the user associated with the token

In addition, we would want the api server to respond with the following standard HTTP Status responses across all api calls

  1. HTTP 200: Any successful REST api call along with its specific json data
  2. HTTP 400: Any api that results in validation errors like “invalid username”, “empty password” etc. along with json data with list of property errors
    “{ [propertyName:””, messageCode:””, message:””], [propertyName:””,messageCode:””].. }
  3. HTTP 404: Any non-existent REST api request using json along with json response of
    { messageCode:”api.notfound”, message:”locale based message desc”}
  4. HTTP 500: Any REST api request resulting in internal error along with json response of
    {messageCode:”api.internal.error”, message:”locale based message” }
  5. HTTP 403: Any REST api request resulting in forbidden
  6. HTTP 405: Any REST api request using invalid method i.e using GET instead of POST/PUT

Any REST API that supports pagination should respond data in the following format, with results containing the appropriate list items and accept “filter”,”offset”,”sort” & “order” as parameters.

{ 
      "from":10, 
        "to":15, 
"totalCount":100,
     results: [
       { prop1:"", prop2:"" },
       { prop1:"", prop2:"" },
       { prop1:"", prop2:"" },
       { prop1:"", prop2:"" },
       { prop1:"", prop2:"" }
     ]
}

Here is sample input/output for the api endpoints for successful responses

Input Output
[POST] /api/v0/members/signup: Create a new User
curl –X POST –d ‘{
“clientId”:”my-client”,
“username”:”abcdef”,
“password”:”123456″,
“email”:”abcd@ef.com”,
“firstName”:”abcd”
}’
{“member”: {
“description”: “”,
“email”: “abcd@ef.com”,
“firstName”: “abcd”,
“id”: 5,
“lastName”: “”,
“phone”: 0,
“socialAccounts”: []
},
“token”: {
“access_token”: “7224..”,
“expires_in”: 43199,
“refresh_token”: “ebff..”,
“scope”: “read,write”,
“token_type”: “bearer”
} }
[POST] /api/v0/auth/login: Member Login
curl –X POST –d ‘{
“clientId”: “my-client”,
“username”: “abcdef”,
“password”: “123456”
}’
{“member”: {
“description”: “”,
“email”: “abcd@ef.com”,
“firstName”: “abcd”,
“id”: 5,
“lastName”: “”,
“phone”: 0,
“socialAccounts”: []
},
“token”: {
“access_token”: “7224..”,
“expires_in”: 43199,
“refresh_token”: “ebff..”,
“scope”: “read,write”,
“token_type”: “bearer”
} }
[  GET] /api/v0/members/{memberId}: Get Member
curl -H     “Authorization: Bearer 7224…” http://../api/v0/members/5 {
“description”: “”,
“email”: “abcd@ef.com”,
“firstName”: “abcd”,
“id”: 5,
“lastName”: “”,
“phone”: 0,
“socialAccounts”: []
}
[  PUT] /api/v0/members/{memberId}: Update Member
curl –X PUT –H “Authorization: Bearer 7224…”
-d { “id”: 5, “firstName”: “fname”, “lastName”: “lname”,”phone”: 0, “description”: “desc”} http://../api/v0/members/5
{
“description”: “desc”,
“email”: “abcd@ef.com”,
“firstName”: “fname”,
“id”: 5,
“lastName”: “lname”,
“phone”: 0,
“socialAccounts”: []
}


We will walk thru the implementation in the following sequence.

      1. Implement OAuth2 endpoints using spring security oauth2 provider plugin along with sample endpoints access controlled by client/user tokens
      2. Annotate your API with Swagger-UI
      3. Create API for Member Create/Read/Update
      4. Write Unit tests for Member Create/Read/Update
      5. Create API for Auth login/logout & Write Unit Tests
      6. Integrating with Email plugin

Before we start ensure that your environment is all set

      • Install Grails 2.4.5
      • Create a starter app using grails
        mkdir ~/work;
        cd ~/work;
        grails create-app starter-app-rest-grails;
        cd starter-app-rest-grails;
        grails run-app;  // you should now see
        ...
        ======================================================================
        | Server running. Browse to http://localhost:8080/starter-app-rest-grails
        

1. Create API for Member Create/Read/Update

Lets make a list of features that we want, just so it would guide us in our design.

  Feature
Login Domain
1a Ensure that password is getting encrypted while saving
1b We shouldn’t be able to create new Login with same username
1c We should have the ability to expire an account
1d We should have the ability to lock an account
Member Domain
2a Each member should have a unique UUID to identify them using URL
2b We cannot have two members with same email
2c A Member cannot exist without a Login
API / MemberController / create
3a Registration with an existing username should respond with 400
3b Registration with an existing email should respond with 400
3c Invalid email syntax should return 400
3d Successful registration should respond with 200 along with member info

Given the list of features, we will start off with the following ER Model

login-member-ermodel

Our next step is to create a domain classes in grails.

grails create-domain-class com.nbos.core.Login
grails create-domain-class com.nbos.core.Member

Add the necessary properties for Login.groovy

package com.nbos.core

class Login {

    def springSecurityService

    String username
    String password

    boolean enabled;
    boolean accountExpired;
    boolean accountLocked;
    boolean passwordExpired;

    static constraints = {
        username blank: false, unique: true
        password blank: false
    }

    static mapping = {
        password column: 'password'
    }

    def getAuthorities() {
        [];
    }

    def beforeInsert() {
        encodePassword()
    }

    def beforeUpdate() {
        if (isDirty('password')) {
            encodePassword()
        }
    }

    protected void encodePassword() {
        password = springSecurityService.encodePassword(password)
    }

    @Override
    def String toString(){
        "Login[${id},${username}]"
    }
}

Add the necessary properties for Member.groovy

package com.nbos.core

import org.apache.commons.lang.builder.HashCodeBuilder

class Member {

    Login login
    String uuid
    String firstName
    String lastName
    String email
    Long phone
    String description

    def memberService

    static belongsTo = [login:Login]

    static constraints = {
        email(nullable: true)
        lastName(nullable: true)
        phone(nullable: true)
        description(nullable: true)
        uuid(nullable: true)
    }

    static mapping = {
    }

    @Override
    boolean equals(other){
        if (!(other instanceof Member)) return false;
        if ( id && other.id && id == other.id ) return true;
        if ( email == other.email ) return true;
        return false;
    }

    @Override
    int hashCode(){
        def builder = new HashCodeBuilder();
        if(id) { builder.append(id) }
        else { builder.append(email) }
        builder.toHashCode()
    }

    def beforeInsert() {
        uuid = memberService.generateUUID()
    }

    @Override
    def String toString() {
        "member: ${id}, ${login}"
    }
}

Time to create service classes

grails create-service com.nbos.core.MemberService
grails create-service com.nbos.core.TransactionService

And its simple implementation for each of these.

package com.nbos.core

class MemberService {
    def generateUUID() {
        UUID.randomUUID().toString()
    }
}

The intent of creating TransactionService is to help us in unit testing of controllers, the idea is, we could check that “transactionService.rollback” is being called in case of exceptions.

package com.nbos.core

import org.springframework.transaction.TransactionStatus

class TransactionService {

    def rollback(TransactionStatus status) {
        status.setRollbackOnly();
    }
}

Lets Create our first API to create a Member

grails create-controller com.nbos.core.MemberRestfulController

Add an action to create Member.

package com.nbos.core

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import grails.transaction.Transactional
import grails.validation.Validateable

class MemberRestfulController {
    static responseFormats = ['json', 'xml']
    static allowedMethods = [create: 'POST']

    static namespace = 'v0'

    def transactionService
    def memberService
    def mailService
    def securityService

    def create() {
        MemberRegistrationCommand cmd = new MemberRegistrationCommand()
        bindData(cmd, request.getJSON())
        try {
            if (!cmd.validate()) {
                response.setStatus(400)
                respond cmd.errors.collect { [ messageCode:it.code, message:message(code:it.code,args:it.arguments), propertyName:it.field] }
                return
            }
            Member m = cmd.createMember()
            respond [ firstName:m.firstName, lastName:m.lastName, id:m.id, email:m.email ]

        } catch (e) {
            transactionService.rollback(transactionStatus)
            response.setStatus(500)
            log.error("Signup Internal Error : ${e.getMessage()}")
            respond [messageCode:'internal.error', message:message(code:'internal.error',args:[])]
        }
    }
}

@Validateable
class MemberRegistrationCommand {
    String clientId
    String username
    String email
    String password
    String firstName
    String lastName
    static constraints = {
        clientId nullable:true, validator: { val, cmd, errors ->
            Object[] args = new Object[1]
            args[0] = val
            if(!val){
                errors.rejectValue("clientId", "clientId.empty", args, null)
            }
        }
        username nullable: true, validator: { val, cmd, errors ->
            Object[] args = new Object[1]
            args[0] = val
            if (!val) {
                errors.rejectValue("username", "user.username.null", args, null)
            } else if (Login.findByUsername(val)) {
                errors.rejectValue("username", "user.username.exists", args, null)
            }
        }
        email nullable: true, validator: { val, cmd, errors ->
            Object[] args = new Object[1]
            args[0] = val
            if (val) {
                def pattern = /[_A-Za-z0-9-]+(.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(.[A-Za-z0-9]+)*(.[A-Za-z]{2,})/
                if ( ! val ==~ pattern ) {
                    errors.rejectValue("email", "user.email.wrongformat", args, null)
                } else {
                    def member = Member.findByEmail(val)
                    if (member) {
                        errors.rejectValue("email", "user.email.exists", args, null)
                    }
                }
            }
        }
        password nullable: true, validator: { val, cmd, errors ->
            Object[] args = new Object[1]
            args[0] = val
            if (!val) {
                errors.rejectValue("password", "user.password.null", args, null)
            }
        }
        firstName nullable: true, validator: { val, cmd, errors ->
            Object[] args = new Object[1]
            args[0] = val
            if (!val) {
                errors.rejectValue("firstName", "user.firstName.null", args, null)
            }
        }
        lastName nullable: true
    }

    def createMember() {
        Login login = new Login(username: username, password: password, enabled: true)
        login.save(failOnError: true, flush: true)
        Member m = new Member(login: login, firstName: firstName, lastName: lastName, email: email)
        m.save(failOnError: true, flush: true)
    }
}

 

We will continue this post with creating tests for the features that we have built.