Supporting authentication and authorization in your mobile app

Configuring your server for authentication and authorization using {idm-name}

Using the {keycloak-service} service and the @aerogear/voyager-keycloak module, it is possible to add security to a Data Sync 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 {idm-name} 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 Data Sync Server using {idm-name}

Procedure
  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 an 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 - An article or a blog post

  • Author - Represents the person that authored a Post

There are two queries:

  • allPosts - Returns a list of posts

  • getAuthor - Returns details about an Author

There are two mutations:

  • editPost - Edits an existing post

  • deletePost - 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 can 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 cannot request the address or age fields.

Authentication Over Websockets using {idm-name}

Prerequisites:

This section describes how to implement authentication and authorization over websockets with {idm-name}. For more information on authentication over websockets, read Apollo’s Authentication Over Websocket documention.

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 {idm-name} Authentication in Voyager Client to ensure the {idm-name} token is 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 {idm-name} 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 {idm-name} securityService is created and how it is passed into createSubscriptionServer. This enables {idm-name} authentication on all subscriptions.

{idm-name} Authorization in Subscriptions

The {idm-name} 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, by using headers or by using 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 ways can be set by the authContextProvider configuration option. For 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.