This Vert.x service provides authentication and authorisation functionality and the concept of a login session for use in your Vert.x applications

The auth service provides a common interface that can be backed by different auth providers.

Vert.x ships with a default implementation that uses Apache Shiro but you can provide your own implementation by implementing the AuthProvider interface.

The AuthProvider interface can also be used directory in your applications if you just want raw authentication and authorisation functionality without the concept of a login session.

The Vert.x Apache Shiro implementation currently allows user/role/permission information to be accessed from simple properties files or LDAP servers.

Basic concepts

Authentication (aka log in) means verifying the identity of a user.

Authorisation means verifying a user has the correct access rights for some resource.

The service uses a familiar user/role/permission model that you will probably know already:

Users can have zero or more roles, e.g. "manager", "developer".

Roles can have zero or more permissions, e.g. a manager might have permission "approve expenses", "conduct_reviews", and a developer might have a permission "commit_code".

Setting up the service

As with other services you can use the service either by deploying it as a verticle somewhere on your network and interacting with it over the event bus, either directly by sending messages, or using a service proxy, e.g.

Somewhere you deploy it:

// Deploy service - can be anywhere on your network
def options = [
  config:config
]

vertx.deployVerticle("service:io.vertx.shiro-auth-service", options, { res ->
  if (res.succeeded()) {
    // Deployed ok
  } else {
    // Failed to deploy
  }
})

Now you can either send messages to it directly over the event bus, or you can create a proxy to the service from wherever you are and just use that:

import io.vertx.groovy.ext.auth.AuthService

def proxy = AuthService.createEventBusProxy(vertx, "vertx.auth")

// Now do stuff with it:

def principal = [
  username:"tim"
]
def credentials = [
  password:"sausages"
]

proxy.login(principal, credentials, { res ->

  // etc

})

Alternatively you can create an instance of the service directly and just use that locally:

import io.vertx.groovy.ext.auth.shiro.ShiroAuthService
import io.vertx.ext.auth.shiro.ShiroAuthRealmType

def config = [:]
config."properties_path" = "classpath:test-auth.properties"

def authService = ShiroAuthService.create(vertx, ShiroAuthRealmType.PROPERTIES, config)

authService.start()

// Now do stuff with it:

def principal = [
  username:"tim"
]
def credentials = [
  password:"sausages"
]

authService.login(principal, credentials, { res ->

  // etc

})

If you create an instance this way you should make sure you start it with start before you use it.

However you do it, once you’ve got your service you can start using it.

TODO these docs need to be refactored to talk about AuthProvider and AuthService as different ways of using it

Shiro Auth Service

As previously mentioned we provide an implementation of the Auth service that uses Apache Shiro to perform the actual auth.

To use this, you should use ShiroAuthService.

This currently supports properties file based user/role/permission information and using LDAP, and you can also pass in a pre-existing Shiro Realm instance or implement ShiroAuthRealm to implement a different method of auth using Shiro.

Shiro Auth Service Verticle

As with most services you can deploy the service somewhere on your network and interact with it via a proxy, here’s an example of deploying a Shiro auth service verticle:

vertx.deployVerticle("service:io.vertx.shiro-auth-service")

Properties auth realm

The properties auth realm gets user/role/permission information from a properties file.

Here’s an example that uses the out of the box properties auth realm:

import io.vertx.groovy.ext.auth.shiro.ShiroAuthService
import io.vertx.ext.auth.shiro.ShiroAuthRealmType

def config = [:]
config."properties_path" = "classpath:test-auth.properties"

def authService = ShiroAuthService.create(vertx, ShiroAuthRealmType.PROPERTIES, config)

authService.start()

// Now do stuff with it:

def principal = [
  username:"tim"
]
def credentials = [
  password:"sausages"
]

authService.login(principal, credentials, { res ->

  // etc

})

The properties auth realm will, by default, look for a file called vertx-users.properties on the classpath.

If you want to change this, you can use the properties_path configuration element to define how the properties file is found.

The default value is classpath:vertx-users.properties.

If the value is prefixed with classpath: then the classpath will be searched for a properties file of that name.

If the value is prefixed with file: then it specifies a file on the file system.

If the value is prefixed with url: then it specifies a URL from where to load the properties.

The properties file should have the following structure:

Each line should either contain the username, password and roles for a user or the permissions in a role.

For a user line it should be of the form:

user.{username}={password},{roleName1},{roleName2},...,{roleNameN}

For a role line it should be of the form:

role.{roleName}={permissionName1},{permissionName2},...,{permissionNameN}

Here’s an example:

user.tim = mypassword,administrator,developer
user.bob = hispassword,developer
user.joe = anotherpassword,manager
role.administrator=*
role.manager=play_golf,say_buzzwords
role.developer=do_actual_work

When describing roles a wildcard * can be used to indicate that the role has all permissions

LDAP auth realm

The LDAP auth realm gets user/role/permission information from an LDAP server.

The following configuration properties are used to configure the LDAP realm:

ldap-user-dn-template

this is used to determine the actual lookup to use when looking up a user with a particular id. An example is uid={0},ou=users,dc=foo,dc=com - the element {0} is substituted with the user id to create the actual lookup. This setting is mandatory.

ldap_url

the url to the LDAP server. The url must start with ldap:// and a port must be specified. An example is ldap:://myldapserver.mycompany.com:10389

ldap-authentication-mechanism

TODO

ldap-context-factory-class-name

TODO

ldap-pooling-enabled

TODO

ldap-referral

TODO

ldap-system-username

TODO

ldap-system-password

TODO

Using non Shiro Auth implementations

If you want to use a different auth provider with the Auth service, you should implement AuthProvider.

You can then create a local instance of the AuthService with:

import io.vertx.groovy.ext.auth.AuthService

def authService = AuthService.create(vertx, myAuthProvider)

authService.start()

Or to to deploy an verticle instance:

def config = [:]
config."provider_class_name" = "com.mycompany.myproject.MyAuthProviderClass"
config."your_config_property" = "blah"

def options = [
  config:config
]

vertx.deployVerticle("service:io.vertx.auth-service", options)

Using the API

The auth service API is described with AuthService.

It contains method to login and check roles and permissions.

Authentication - login / logout

You use login to login a user. The arguments to log-in are a JsonObject representing the principal (principal is a fancy name for a unique id, e.g. username representing the user), and another JsonObject representing the credentials (e.g. password) of the user.

Often the principal will just contain a username string field - the value containing the username and this is what is expected by the out of the box Apache Shiro provider, but other providers might represent principals in other ways.

Similarly, the credentials will often just be a password string field - the value containing a password but other providers might use other data for credentials that’s why we keep it as a general JSON object.

The result of the login is returned in the result handler. If the login is successful a string login-ID will be returned as the result. This is a unique secure UUID that identifies the login session. The login ID should be used if you later want to authorise the user, i.e. check whether they have permissions or roles.

Here’s an example of a login:

def principal = [
  username:"tim"
]
def credentials = [
  password:"sausages"
]

authService.login(principal, credentials, { res ->

  if (res.succeeded()) {

    // Login successful!

    // The login ID is needed if you later want to authorise a user

    def loginID = res.result()

  } else {

    // Login failed.

    def reason = res.cause().getMessage()

  }
})

The login session ID provided at login will be valid as long as the login hasn’t timed out or been explicitly logged out.

The default time it remains valid is 30 minutes. If you want to use a different value of timeout you can specify that by calling loginWithTimeout.

To prevent a login timing out, you can call refreshLoginSession specifying the login ID. The login will timeout if it remains unrefreshed for greater than the timeout period.

authService.refreshLoginSession(loginID, { res ->

  if (res.succeeded()) {

    // Refreshed ok

  } else {

    // Not refreshed ok - probably the login has already timed out or doesn't exist.
  }
})

You can explicitly logout a user with logout specifying the login ID:

authService.logout(loginID, { res ->

  if (res.succeeded()) {

    // Logged out ok

  } else {

    // Failed to logout - probably the login has already timed out or doesn't exist.

  }
})

Authorisation

Authorisation means checking whether the user has the right roles or permissions.

In order to check roles or permissions the user must first be logged-in and you must have a valid login session ID as described in the previous section.

To check if a user has a specific role you use hasRole specifying the login ID and the role.

The result of the check is returned in the handler. If the check didn’t occur - e.g. the login ID is not valid, a failure will be returned in the handler, otherwise it will return a boolean - true if the user has the role or false if they don’t have the role.

authService.hasRole(loginID, "manager", { res ->

  if (res.succeeded()) {

    def hasRole = res.result()

    if (hasRole) {

      // do something

    } else {

      // do something else

    }

  } else {

    // Something went wrong - maybe the user is not logged in?
  }

})

You can also check multiple roles at the same time with hasRoles. In this case you will return a true result only if the user has all the specified roles.

In the same way as checking roles, you can check permissions too. To this you use hasPermission and hasPermissions in the exact same way as roles.

Authorisations are cached for the length of the login. This means that the first time you do authorisation for a user it will go the auth provider, but the second time you do it with the same roles and permissions it will not call the auth provider but will return the cached value.

This allows better performance but bear in mind that if the roles or permissions for a user change in the provider while the login session is valid and when they have already been cached in the auth service, then the auth service won’t see the changes in the provider until a new login session is started.