Supporting authentication and authorization in your mobile app

Configuring your server for authentication and authorization using Keycloak

Using the Identity Management service and the @aerogear/voyager-keycloak module, it is possible to add security to a Voyager Server application.

The @aerogear/voyager-keycloak module provides the following features out of the box:

  • Authentication - Ensure only authenticated users can access your server endpoints, including the main GraphQL endpoint.

  • Authorization - Use the @hasRole() directive within the GraphQL schema to implement role based access control (RBAC) on the GraphQL level.

  • Integration with GraphQL context - Use the context object within the GraphQL resolvers to access user credentials and several helper functions.

Prerequisites
  • There is a Keycloak service available.

  • You must add a valid keycloak.json config file to your project.

    • Create a client for your application in the Keycloak administration console.

    • Click on the Installation tab.

    • Select Keycloak OIDC JSON for Format option, and click Download.

Protecting Voyager Server using Keycloak

  1. Import the @aerogear/voyager-keycloak module

    const { KeycloakSecurityService } = require('@aerogear/voyager-keycloak')
  2. Read the Keycloak config and pass it to initialise the KeycloakSecurityService.

    const keycloakConfig = JSON.parse(fs.readFileSync(path.resolve(__dirname, './path/to/keycloak.json')))
    const keycloakService = new KeycloakSecurityService(keycloakConfig)
  3. Use the keycloakService instance to protect your app:

    const app = express()
    keycloakService.applyAuthMiddleware(app)
  4. Configure the Voyager server so that the keycloakService is used as the security service:

    const voyagerConfig = {
      securityService: keycloakService
    }
    const server = VoyagerServer(apolloConfig, voyagerConfig)

The Keycloak Example Server Guide has an example server based off the instructions above and shows all of the steps needed to get it running.

Protecting Subscriptions using Keycloak

If you have set up a subscription server as described in Realtime Updates and you have set up Identity Management as described above then your application should make use of the onConnect function to validate the token provided by the client. An example implementation of onConnect is shown below with a KeycloakService set up as shown above in Setting Up Keycloak Protection in Voyager Server.

  new SubscriptionServer ({
    execute,
    subscribe,
    onConnect: async connectionParams => {
      return await keycloakService.validateToken(connectionParams)
    },
    schema: server.schema
  }, ...)

Using the hasRole directive in a schema

The Voyager Keycloak module provides the @hasRole directive to define role based authorisation in your schema. The @hasRole directive is a special annotation that can be applied to

  • Fields

  • Queries

  • Mutations

  • Subscriptions

The @hasRole usage is as follows:

  • @hasRole(role: String)

  • Example - @hasRole(role: "admin"])

  • If the authenticated user has the role admin they will be authorized.

  • @hasRole(role: [String])

  • Example - @hasRole(role: ["admin", "editor"])

  • If the authenticated user has at least one of the roles in the list, they will be authorized.

The default behaviour is to check client roles. For example, @hasRole(role: "admin") will check that user has a client role called admin. @hasRole(role: "realm:admin") will check if that user has a realm role called admin

The syntax for checking a realm role is @hasRole(role: "realm:<role>"). For example, @hasRole(role: "realm:admin"). Using a list of roles, it is possible to check for both client and realm roles at the same time.

Example: Using the @hasRole Directive to Apply Role Based Authorization in a Schema

The following example demonstrates how the @hasRole directive can be used to define role based authorization on various parts of a GraphQL schema. This example schema represents publishing application like a news or blog website.

type Post {
  id: ID!
  title: String!
  author: Author!
  content: String!
  createdAt: Int!
}

type Author {
  id: ID!
  name: String!
  posts: [Post]!
  address: String! @hasRole(role: "admin")
  age: Int! @hasRole(role: "admin")
}

type Query {
  allPosts:[Post]!
  getAuthor(id: ID!):Author!
}

type Mutation {
  editPost:[Post]! @hasRole(role: ["editor", "admin"])
  deletePost(id: ID!):[Post] @hasRole(role: "admin")
}

There are two types:

  • Post - This might be an article or a blog post

  • Author - This would represent the person that authored a Post

There are two Queries:

  • allPosts - This might return a list of posts

  • getAuthor - This would return details about an Author

There are two Mutations:

  • editPost - This would edit an existing post

  • deletePost - This would delete a post.

Role Based Authorization on Queries and Mutations

In the example schema, the @hasRole directive has been applied to the editPost and deletePost mutations. The same could be done on Queries.

  • Only users with the roles editor and/or admin are allowed to perform the editPost mutation.

  • Only users with the role admin are allowed to perform the deletePost mutation.

This example shows how the @hasRole directive can be used on various queries and mutations.

Role Based Authorization on Fields

In the example schema, the Author type has the fields address and age which both have hasRole(role: "admin") applied.

This means that users without the role admin are not authorized to request these fields in any query or mutation.

For example, non admin users are allowed to run the getAuthor query, but they cannot request back the address or age fields.

Implementing authentication and authorization on your client

With Voyager Client, user information can be passed to a Data Sync server application in two ways: headers or tokens.

Headers are used to authentication HTTP requests to the server, which are used for queries and mutations.

Tokens are used to authenticate WebSocket connections, which are used for subscriptions.

Both of them can be set via the authContextProvider configuration option. Here is an example

//get the token value from somewhere, for example the authentication service
const token = "REPLACE_WITH_REAL_TOKEN";

const config = {
  ...
  authContextProvider: function() {
    return {
      header: {
        "Authorization": `Bearer ${token}`
      },
      token: token
    }
  },
  ...
};

//create a new client

For information about how to perform authentication and authorization on the server, see the Server Authentication and Authorization Guide.