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.

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.

Authentication Over Websockets using Keycloak

Prerequisites:

This section describes how to implement Authentication and Authorization over websockets with Keycloak. For more generic documentation on Authentication over Websockets, read Apollo’s Authentication Over Websocket document.

The Voyager Client supports adding token information to connectionParams that will be sent with the first WebSocket message. In the server, this token is used to authenticate the connection and to allow the subscription to proceeed. Read the section on Keycloak Authentication in Voyager Client to ensure the Keycloak token is being sent to the server.

In the server, createSubscriptionServer accepts a SecurityService instance in addition to the regular options that can be passed to a standard SubscriptionServer. The KeycloakSecurityService from @aerogear/voyager-keycloak is used to validate the Keycloak token passed by the client in the initial WebSocket message.

const { createSubscriptionServer } = require('@aerogear/voyager-subscriptions')
const { KeycloakSecurityService } = require('@aerogear/voyager-keycloak')
const keycloakConfig = require('./keycloak.json') // typical Keycloak OIDC installation

const apolloServer = VoyagerServer({
  typeDefs,
  resolvers
})

securityService = new KeycloakSecurityService(keycloakConfig)

const app = express()

keycloakService.applyAuthMiddleware(app)
apolloServer.applyMiddleware({ app })

const server = app.listen({ port }, () =>
  console.log(`🚀 Server ready at http://localhost:${port}${apolloServer.graphqlPath}`)

  createSubscriptionServer({ schema: apolloServer.schema }, {
    securityService,
    server,
    path: '/graphql'
  })
)

The example shows how the Keycloak securityService is created and how it is passed into createSubscriptionServer. This enables Keycloak authentication on all subscriptions.

Keycloak Authorization in Subscriptions

The Keycloak securityService will validate and parse the token sent by the client into a Token Object. This token is available in Subscription resolvers with context.auth and can be used to implement finer grained role based access control.

const resolvers = {
  Subscription: {
    taskAdded: {
      subscribe: (obj, args, context, info) => {
        const role = 'admin'
        if (!context.auth.hasRole(role)) {
          return new Error(`Access Denied - missing role ${role}`)
        }
        return pubSub.asyncIterator(TASK_ADDED)
      }
    },
}

The above example shows role based access control inside a subscription resolver. context.auth is a full Keycloak Token Object which means methods like hasRealmRole and hasApplicationRole are available.

The user details can be accessed through context.auth.content. Here is an example.

{
  "jti": "dc1d6286-c572-43c1-99c7-4f36982b0e56",
  "exp": 1561495720,
  "nbf": 0,
  "iat": 1561461830,
  "iss": "http://localhost:8080/auth/realms/voyager-testing",
  "aud": "voyager-testing-public",
  "sub": "57e1dcda-990f-4cc2-8542-0d1f9aae302b",
  "typ": "Bearer",
  "azp": "voyager-testing-public",
  "nonce": "552c3cba-a6c2-490a-9914-28784ba0e4bc",
  "auth_time": 1561459720,
  "session_state": "ed35e1b4-b43c-438f-b1a3-18b1be8c6307",
  "acr": "0",
  "allowed-origins": [
    "*"
  ],
  "realm_access": {
    "roles": [
      "developer",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "voyager-testing-public": {
      "roles": [
        "developer"
      ]
    },
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "preferred_username": "developer"
}

Having access to the user details (e.g. context.auth.content.sub is the authenticated user’s ID) means it is possible to implement Subscription Filters and to subscribe to more fine grained pubsub topics based off the user details.

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.