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.
-
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
-
Import the
@aerogear/voyager-keycloak
moduleconst { KeycloakSecurityService } = require('@aerogear/voyager-keycloak')
-
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)
-
Use the
keycloakService
instance to protect your app:const app = express() keycloakService.applyAuthMiddleware(app)
-
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.
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.
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/oradmin
are allowed to perform theeditPost
mutation. -
Only users with the role
admin
are allowed to perform thedeletePost
mutation.
This example shows how the @hasRole
directive can be used on various queries and mutations.
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.