Show Navigation

Custom Tenant Resolver by JWT

Learn how to create a custom tenant resolver and use Grails Multi-Tenancy capabilities to switch tenants based on the current logged user JSON Web Token.

Authors: Sergio del Amo

Grails Version: 3.3.1

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

GORM Multi-Tenancy Support includes several Built-In Tenant resolvers. In this guide, you are going to create your own tenant resolver to switch tenants based on the logged user in a Grails rest-api application which uses Spring Security REST plugin.

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.7 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-custom-security-tenant-resolver/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-custom-security-tenant-resolver/complete

3 Writing the Application

This guide uses Multi-Tenancy DISCRIMINATOR mode. To learn more, read Single Database Multi-Tenancy Discriminator Column Guide.

3.1 Configure Multi-Tenancy

Configure Multi-Tenancy by modifying application.yml.

grails-app/conf/application.yml
grails
    gorm:
        multiTenancy:
            mode: DISCRIMINATOR (1)
            tenantResolverClass: demo.CurrentUserByJwtTenantResolver (2)
        reactor:
            # Whether to translate GORM events into Reactor events
            # Disabled by default for performance reasons
            events: false
1 Define Multi-Tenancy mode as DISCRIMINATOR
2 Set the Tenant Resolver class with a custom class which you will write later in this guide.

3.2 Create Domain Class

grails-app/domain/demo/Plan.groovy
package demo

import grails.gorm.MultiTenant

class Plan implements MultiTenant<Plan> { (1)
    String title
    String username

    static mapping = {
        tenantId name: 'username' (2)
    }
}
1 Implement MultiTenant trait to regard this domain class as multi tenant.
2 Setup the tenant identifier as a column named username

Create an interface PlanService which is a GORM Data Service.

grails-app/services/demo/PlanService.groovy
package demo

import grails.gorm.multitenancy.CurrentTenant
import grails.gorm.services.Service
import groovy.transform.CompileStatic

@CompileStatic
@Service(Plan) (1)
@CurrentTenant (2)
interface PlanService {
    List<Plan> findAll()
    Plan save(String title)
    void deleteByTitle(String title)
}
1 Annotate it with the grails.gorm.services.Service annotation with the domain class the service applies to.
2 Resolve the current tenant for the context of this class

3.3 Controller

Create a simple controller which uses with the previously defined PlanService service.

grails-app/controllers/demo/PlanController.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class PlanController {
    PlanService planService

    def index() {
        [planList: planService.findAll()]
    }
}

We render the Plan instances as JSON with the help of JSON Views

grails-app/views/plan/index.gson
import demo.Plan

model {
    Iterable<Plan> planList
}

json tmpl.plan('plan', planList)
grails-app/views/plan/_plan.gson
import demo.Plan

model {
    Plan plan
}

json {
    title plan.title
}

3.4 Wire up Security

Create security domain classes. You could create these classes with Spring Security Core s2-quickstart.

grails-app/domain/demo/User.groovy
package demo

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {

    private static final long serialVersionUID = 1

    String username
    String password
    boolean enabled = true
    boolean accountExpired
    boolean accountLocked
    boolean passwordExpired

    Set<Role> getAuthorities() {
        (UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<Role>
    }

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

    static mapping = {
            password column: '`password`'
    }
}
grails-app/domain/demo/Role.groovy
package demo

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Role implements Serializable {

        private static final long serialVersionUID = 1

        String authority

        static constraints = {
                authority nullable: false, blank: false, unique: true
        }

        static mapping = {
                cache true
        }
}

Create a class UserPasswordEncoderListener which deals with the User’s password encoding.

It uses the @Listener annotation to listen synchronously to Events from GORM.

src/main/groovy/demo/UserPasswordEncoderListener.groovy
package demo

import grails.plugin.springsecurity.SpringSecurityService
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
import org.grails.datastore.mapping.engine.event.EventType
import org.grails.datastore.mapping.engine.event.PreInsertEvent
import org.grails.datastore.mapping.engine.event.PreUpdateEvent
import org.hibernate.event.spi.PreDeleteEvent
import org.springframework.beans.factory.annotation.Autowired
import grails.events.annotation.gorm.Listener
import groovy.transform.CompileStatic

@CompileStatic
class UserPasswordEncoderListener {

    @Autowired
    SpringSecurityService springSecurityService

    @Listener(User)
    void onPreInsertEvent(PreInsertEvent event) {
        encodeUserPasswordForEvent(event)
    }

    @Listener(User)
    void onPreUpdateEvent(PreUpdateEvent event) {
        encodeUserPasswordForEvent(event)
    }

    private void encodeUserPasswordForEvent(AbstractPersistenceEvent event) {
        if (event.entityObject instanceof User) {
            User u = (event.entityObject as User)
            if (u.password && ((event instanceof  PreInsertEvent) || (event instanceof PreUpdateEvent && u.isDirty('password')))) {
                event.getEntityAccess().setProperty("password", encodePassword(u.password))
            }
        }
    }

    private String encodePassword(String password) {
        springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
    }
}

Define the previous listener as a bean:

grails-app/conf/spring/resources.groovy
import demo.UserPasswordEncoderListener
beans = {
    userPasswordEncoderListener(UserPasswordEncoderListener)
}

Configure Spring Security Core in application.yml

grails-app/conf/application.yml
grails
grails:
    plugin:
        springsecurity:
            securityConfigType: InterceptUrlMap
            userLookup:
                userDomainClassName: demo.User
                authorityJoinClassName: demo.UserRole
            authority:
                className: demo.Role
            filterChain:
                chainMap:
                    - # Stateless chain
                        pattern: /**
                        filters: 'JOINED_FILTERS,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'
            interceptUrlMap:
                -
                    pattern: /
                    access:
                        - ROLE_VILLAIN
                -
                    pattern: /error
                    access:
                        - permitAll
                -
                    pattern: /api/login
                    access:
                        - ROLE_ANONYMOUS
                -
                    pattern: /api/validate
                    access:
                        - ROLE_ANONYMOUS
                -
                    pattern: /oauth/access_token
                    access:
                        - ROLE_ANONYMOUS
                -
                    pattern: /plan
                    access:
                        - ROLE_VILLAIN

Create several GORM Data Service to work with the Domain Classes involed in the securization of our app.

grails-app/services/demo/RoleService.groovy
package demo

import grails.gorm.services.Service
import groovy.transform.CompileStatic

@CompileStatic
@Service(Role)
interface RoleService {
    void delete(String authority)
    Role findByAuthority(String authority)
    Role saveByAuthority(String authority)
}
grails-app/services/demo/UserService.groovy
package demo

import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
interface IUserService {
    User save(String username, String password)
}

@Service(User)
@CompileStatic
abstract class UserService implements IUserService {

    @Transactional
    void deleteUser(User userParam) {
        UserRole.where { user == userParam }.deleteAll()
        userParam.delete()
    }
}
grails-app/services/demo/UserRoleService.groovy
package demo

import grails.gorm.services.Query
import grails.gorm.services.Service
import groovy.transform.CompileStatic

@Service(UserRole)
@CompileStatic
interface UserRoleService {

    @Query("""select $user.username from ${UserRole userRole}
    inner join ${User user = userRole.user}
    inner join ${Role role = userRole.role}
    where $role.authority = $authority""")
    List<String> findAllUsernameByAuthority(String authority)

    UserRole save(User user, Role role)
}

Create a service to handle User creation with a specific role.

grails-app/services/demo/AbstractUserWithRoleService.groovy
package demo

import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
abstract class AbstractUserWithRoleService {
    RoleService roleService

    UserRoleService userRoleService

    UserService userService

    abstract String getAuthority()

    @Transactional
    User saveVillain(String username, String password) {
        Role role = roleService.findByAuthority(getAuthority())
        if ( !role ) {
            role = roleService.saveByAuthority(getAuthority())
        }
        User user = userService.save(username, password)
        userRoleService.save(user, role)
        user
    }
}

Create a service which extends the previous service:

grails-app/services/demo/VillainService.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class VillainService extends AbstractUserWithRoleService {

    public static final String ROLE_VILLAIN = 'ROLE_VILLAIN'

    @Override
    String getAuthority() {
        ROLE_VILLAIN
    }
}

3.5 Custom Tenant Resolver

The following table contains all of the TenantResolver implementations that ship with GORM and are usable out of the box. The package name has been shorted from org.grails.datastore.mapping to o.g.d.m for brevity:

name description

o.g.d.m.multitenancy.resolvers.FixedTenantResolver

Resolves against a fixed tenant id

o.g.d.m.multitenancy.resolvers.SystemPropertyTenantResolver

Resolves the tenant id from a system property called gorm.tenantId

o.g.d.m.multitenancy.web.SubDomainTenantResolver

Resolves the tenant id from the subdomain via DNS

o.g.d.m.multitenancy.web.CookieTenantResolver

Resolves the current tenant from an HTTP cookie named gorm.tenantId by default

o.g.d.m.multitenancy.web.SessionTenantResolver

Resolves the current tenant from the HTTP session using the attribute gorm.tenantId by default

However, you have the flexibility to create your a custom tenant resolver is easy. In order to do that, create a class which implements org.grails.datastore.mapping.multitenancy.TenantResolver. Given a request such as:

## Request Duplicate
curl "http://localhost:8080/plan" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9....." \
     -H "Accept: application/json"

which contains a JWT token in the Authorization header.

Create a Custom Tenant Resolver which extracts the JWT token, re-hydrates it and extracts the username.

src/main/groovy/demo/CurrentUserByJwtTenantResolver.groovy
package demo

import grails.plugin.springsecurity.rest.token.storage.TokenStorageService
import groovy.transform.CompileStatic
import org.grails.datastore.mapping.multitenancy.TenantResolver
import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.web.context.request.RequestAttributes
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletWebRequest

import javax.servlet.http.HttpServletRequest

@CompileStatic
class CurrentUserByJwtTenantResolver implements TenantResolver {

    public static final String HEADER_NAME = 'Authorization'
    public static final String HEADER_VALUE_PREFFIX = 'Bearer '

    String headerName = HEADER_NAME
    String headerValuePreffix = HEADER_VALUE_PREFFIX

    @Autowired
    TokenStorageService tokenStorageService

    @Override
    Serializable resolveTenantIdentifier() throws TenantNotFoundException {

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes()
        if(requestAttributes instanceof ServletWebRequest) {

            HttpServletRequest httpServletRequest = ((ServletWebRequest) requestAttributes).getRequest()
            String token = httpServletRequest.getHeader(headerName.toLowerCase())
            if ( !token ) {
                throw new TenantNotFoundException("Tenant could not be resolved from HTTP Header: ${headerName}")
            }

            if (token.startsWith(headerValuePreffix)) {
                token = token.substring(headerValuePreffix.length())
            }
            UserDetails userDetails = tokenStorageService.loadUserByToken(token)
            String username = userDetails?.username
            if ( username ) {
                return username
            }
            throw new TenantNotFoundException("Tenant could not be resolved from HTTP Header: ${headerName}")
        }
        throw new TenantNotFoundException("Tenant could not be resolved outside a web request")
    }
}

Define the Custom Tenant Resolver as a bean:

grails-app/conf/spring/resources.groovy
import demo.CurrentUserByJwtTenantResolver
beans = {
    currentUserByJwtTenantResolver(CurrentUserByJwtTenantResolver)
}

Create an Integration Test to test the custom Tenant Resolver.

src/integration-test/groovy/demo/CurrentUserByJwtTenantResolverSpec.groovy
package demo

import grails.testing.mixin.integration.Integration
import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletWebRequest
import spock.lang.Specification
import spock.lang.IgnoreIf

@IgnoreIf( { System.getenv('TRAVIS') as boolean } )
@Integration
class CurrentUserByJwtTenantResolverSpec extends Specification {

    @Autowired
    CurrentUserByJwtTenantResolver currentUserTenantResolver

    void "Test HttpHeader resolver throws an exception outside a web request"() {
        when:
        currentUserTenantResolver.resolveTenantIdentifier()

        then:
        def e = thrown(TenantNotFoundException)
        e.message == "Tenant could not be resolved outside a web request"
    }


    void "Test not tenant id found"() {
        setup:
        def request = new MockHttpServletRequest("GET", "/foo")
        RequestContextHolder.setRequestAttributes(new ServletWebRequest(request))

        when:
        currentUserTenantResolver.resolveTenantIdentifier()

        then:
        def e = thrown(TenantNotFoundException)
        e.message == "Tenant could not be resolved from HTTP Header: ${CurrentUserByJwtTenantResolver.HEADER_NAME}"

        cleanup:
        RequestContextHolder.setRequestAttributes(null)
    }

    void "Test HttpHeader value is the tenant id when a request is present"() {

        setup:
        MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo")
        String jwt = '''\
eyJhbGciOiJIUzI1NiJ9.eyJwcmluY2lwYWwiOiJINHNJQUFBQUFBQUFBSlZTdjA4VVFSUitleHhDZ2xF\
d2tjUUNHN0V6ZXdtV1Z3RkJBcGtjeHZNc01OSE03VDZXZ2RtWmRXYjJ1R3ZJVlZKUVFGUVNFbHBMXC9oT\
nRcL0FPTUZMVFV0THhaT1BhMElVNjFlZlB0OSt2TjZRV01XZ012RThPRnRHRW04MFNvMEdaR3FNUmlsQn\
ZoZW1GdTBjVG9Dc1J5QVd6UkJLNVBVSUdBUVVYRURoNnhMZDdoTmNsVlVsdHJiMkhrNmwwRGM5b2tONHd\
iaHFlNG84MTJlTXNkYVlOXC9DWlRVd2ZjS2pLM0RGSThpblN2WDBHcXBtd21EOFRwTWxqT21vMjBcL2Vo\
elJEU29udUxURDBERlV2QzB4WmpEQmM3ZXBTVldnZGZEdzJtenVoS3cxMGRVWmpHZmNXbkwzVDVLbTg5Yj\
l2YmVwS01FbjJJVnFOd3ZvVUhmUFBUVDBQT0dpbHBKU0M2M3NiRXVsT2hZYndvc1RmM1wvbXk2K0RrMzZy\
QWtDZHZMajduM0wrWkFINlB6NWNQaTJLRGlJSDAwUFdTMWk5bTVHYnFaTDVyVUd2XC9QdjQ5ZGVqaTczM0\
k2VHNFYVwvK2Z4K3o4emZOOVJaMW1uSERuUjdhRWRIdVZQMDNrU1wvY1RUN1lRaTlzaWpTVFNDOUtPWXh2\
SlVwaWlsczFXZzc2ZG5EXC96UnBiK3ZodWhiSDVsWVlmM090UWRtMUkrRUdSMnk4c1pKcld0WDkrK1BQZ\
zJSOGlXWVhSRHBjNVV1MlRKYWlScDIwMG4wK1BaaWErbmUwWElRWVArZ3JPZUdRVkZBTUFBQT09Iiwic3\
ViIjoidmVjdG9yIiwicm9sZXMiOlsiUk9MRV9WSUxMQUlOIl0sImlhdCI6MTUwNDc4MTEyNX0.cf5rGNrNolchQ3QyMsPB544fwzYGiihBkRF8KU6soxc'''

        request.addHeader('Authorization', "Bearer $jwt")
        RequestContextHolder.setRequestAttributes(new ServletWebRequest(request))


        when:
        def tenantId = currentUserTenantResolver.resolveTenantIdentifier()

        then:
        tenantId == "vector"

        cleanup:
        RequestContextHolder.setRequestAttributes(null)
    }
}

3.6 Functional Test

Create a functional test which verifies that we can consume the API switching users.

src/integration-test/groovy/demo/PlanControllerSpec.groovy
package demo

import grails.gorm.multitenancy.Tenants
import grails.plugins.rest.client.RestBuilder
import grails.testing.mixin.integration.Integration
import spock.lang.Specification
import spock.lang.IgnoreIf

@IgnoreIf( { System.getenv('TRAVIS') as boolean } )
@Integration
class PlanControllerSpec extends Specification {
    PlanService planService
    UserService userService
    VillainService villainService
    RoleService roleService

    RestBuilder rest = new RestBuilder()

    String accessToken(String u, String p) {
        def resp = rest.post("http://localhost:${serverPort}/api/login") {
            accept('application/json')
            contentType('application/json')
            json {
                username = u
                password = p
            }
        }
        if ( resp.status == 200 ) {
            return resp.json.access_token
        }
        null
    }

    def "Plans for current logged user are retrieved"() {
        given:
        User vector = villainService.saveVillain('vector', 'secret')
        User gru = villainService.saveVillain('gru', 'secret')
        Tenants.withId("gru") { (1)
            planService.save('Steal the Moon')
        }
        Tenants.withId("vector") {
            planService.save('Steal a Pyramid')
        }

        when: 'login with the gru'
        String gruAccessToken = accessToken('gru', 'secret')

        then:
        gruAccessToken

        when:
        def resp = rest.get("http://localhost:${serverPort}/plan") {
            accept('application/json')
            header('Authorization', "Bearer ${gruAccessToken}")
        }

        then:
        resp.status == 200
        resp.json.toString() == '[{"title":"Steal the Moon"}]'

        when: 'login with the vector'
        String vectorAccessToken = accessToken('vector', 'secret')

        then:
        vectorAccessToken

        when:
        resp = rest.get("http://localhost:${serverPort}/plan") {
            accept('application/json')
            header('Authorization', "Bearer ${vectorAccessToken}")
        }

        then:
        resp.status == 200
        resp.json.toString() == '[{"title":"Steal a Pyramid"}]'

        cleanup:
        Tenants.withId("gru") { (1)
            planService.deleteByTitle('Steal the Moon')
        }
        Tenants.withId("vector") {
            planService.deleteByTitle('Steal the Pyramid')
        }
        userService.deleteUser(gru)
        userService.deleteUser(vector)
        roleService.delete(VillainService.ROLE_VILLAIN)
    }
}
1 You can use a specific tenant id using the withId method

4 Run the tests

To run the tests:

./grailsw
grails> test-app
grails> open test-report

or

./gradlew check
open build/reports/tests/index.html

5 Help with Grails

OCI sponsored the creation of this Guide. OCI offers several Grails services:

Free consultation

The OCI Grails Team includes Grails co-founders, Jeff Scott Brown and Graeme Rocher. Check our Grails courses and learn from the engineers who developed, matured and maintain Grails.

Grails OCI Team