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.
-
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}
-
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 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.
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/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 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.