User Management with GraphQL

User Management with GraphQL

Intro

This tutorial shows how to manage user account data within your applications by connecting to GraphQL endpoints. First ensure that you are running version 7.0 or later of the Curity Identity Server. The tutorial will explain the setup for an example scenario where a customer portal application is used by administrators to manage user accounts.

Identity Server Setup

When managing user accounts, the Curity Identity Server acts as a Web API, so you must present an access token and use scopes and claims for authorization. In order to provide a secure and flexible solution, there are a few layers, which are explained in the following sections.

Prerequisites

This guide assumes that you have run the Basic Setup Wizard and configured a data source, as well as an Authentication Profile, Token Service Profile and User Management Profile.

GraphQL in the Curity Identity Server is supported for JDBC data sources, and for other data sources you can instead use SCIM Endpoints. This tutorial will assume that user accounts are stored in a PostgreSQL database:

GraphQL Data Source

Add the GraphQL User Management Endpoint

To start using GraphQL for user accounts, first add an endpoint of type um-graphql-api to the User Management Service.

GraphQL Endpoint

Configure Scopes and Claims

This tutorial's client application will use a scope called accounts that contains a groups claim. You must either use the built-in claim name urn:se:curity:claims:admin:groups or the specific claim name groups, and in this tutorial we will use the latter option. It is important to ensure that the scope is then included in access tokens under the Claims Mapper section:

GraphQL Scope and Claim

The groups claim must evalute to an array of strings. This tutorial will use the following hard coded value, though various types of ClaimsValueProvider can be used if required, to assign values according to custom logic. See the Adding Claims tutorial for an example.

GraphQL Groups Claim Value

Create an Authorization Manager

Next navigate to System Authorization and add an Authorization Manager with Type=Group and map it to the accounts scope. The below scope enables full control over all user accounts, though in a real application it is recommended to only assign the permissions needed. The admin value from the groups claim will then map to these authorization rules at runtime:

GraphQL Authorization Manager

Get an Access Token

For the purposes of this tutorial we will use a simple Client Credentials based client to get an access token. A real customer portal would instead use the Code Flow, where the process would be equivalent, to just add the accounts scope to that app's configuration. The test client can be imported from the following XML, and for convenience it also has the introspection capability, to enable us to verify that the access token is issued with the expected groups claim:

<config xmlns="http://tail-f.com/ns/config/1.0">
    <profiles xmlns="https://curity.se/ns/conf/base">
    <profile>
    <id>oauth-dev</id>
    <type xmlns:as="https://curity.se/ns/conf/profile/oauth">as:oauth-service</type>
      <settings>
      <authorization-server xmlns="https://curity.se/ns/conf/profile/oauth">
      <client-store>
      <config-backed>
      <client>
        <id>accounts-client</id>
        <client-name>accounts-client</client-name>
        <secret>Password1</secret>
        <scope>accounts</scope>
        <capabilities>
          <client-credentials/>
          <introspection/>
        </capabilities>
        <use-pairwise-subject-identifiers>
          <sector-identifier>accounts-client</sector-identifier>
        </use-pairwise-subject-identifiers>
      </client>
      </config-backed>
      </client-store>
      </authorization-server>
      </settings>
  </profile>
  </profiles>
</config>

An access token can then be retrieved and introspected using the following curl requests, or alternatively you could perform the same operations using OAuth Tools. These examples use an HTTP setup, to avoid SSL trust issues on a development computer, though of course any deployed system would instead use HTTPS.

OPAQUE_ACCESS_TOKEN=$(curl -s -k -X POST http://localhost:8443/oauth/v2/oauth-token \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials&client_id=accounts-client&client_secret=Password1&scope=accounts' \
| jq -r '.access_token')
echo $OPAQUE_ACCESS_TOKEN

JWT_ACCESS_TOKEN=$(curl -s -k -X POST http://localhost:8443/oauth/v2/oauth-introspect \
-u "accounts-client:Password1" \
-H "Accept: application/jwt" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=$OPAQUE_ACCESS_TOKEN")
echo $JWT_ACCESS_TOKEN

The JWT access token can then be inspected in an online viewer, to ensure that the groups array claim has the expected value:

GraphQL Access Token

Connect with GraphiQL

Now that an access token is available, tools can be used to connect to the GraphQL endpoint of the Curity Identity Server. First download and install the GraphiQL tool, then configure the API endpoint, which in this tutorial is http://localhost:8443/um/graphql/admin. Then use the following initial query to retrieve the GraphQL schema:

query getBaseSchema
{
  __schema {
    queryType {
      fields {
        name
        description
      }
    }
  }
}

Initially there will be an error response with a 401 status, and to resolve this GraphiQL must be instructed to send the access token. Do this by selecting Edit HTTP Headers and entering the opaque access token retrieved earlier:

Graphiql Token

You will then be able to connect successfully and retrieve the schema, and start using GraphQL securely. The following response shows the fields available, and GraphiQL retrieves the schema silently when you first browse to the URL, then uses the data to provide intellisense support:

Graphiql Connected

Working with Account Data

You can retrieve the fields available on the main Account object with this GraphQL introspection command, though you may prefer to just use the intellisense to see which fields are available. The response will include a name and description for each field.

query getAccountSchema
{
   __type(name:"Account") {
      fields {
         name
         description
      }
   }
}

Find a particular user account using one of the lookup methods:

query findAccount
{
  accountByEmail(email: "jane.doe@mycompany.com") {
    id
    name {
      givenName
      middleName
      familyName
      formatted
    }
    preferredLanguage
    displayName
    linkedAccounts {
      value
    }
  }
}

The results are then returned in the standard GraphQL JSON response format, containing the fields requested within a data section:

{
  "data": {
    "accountByEmail": {
      "id": "4d807914-ab51-11ec-b593-0242ac170002",
      "name": {
        "givenName": "Jane",
        "middleName": null,
        "familyName": "Doe",
        "formatted": "Jane Doe"
      },
      "preferredLanguage": null,
      "displayName": null,
      "linkedAccounts": []
    }
  }
}

Some companies will have a large volume of user accounts, so pagination is used when working with lists, to ensure that queries are efficient. Select a list of the first 10 user accounts like this:

query getAccounts
{
   accounts(first: 10) {
    edges {
      node {
        id
        name {
          givenName
          middleName
          familyName
        }
        userName
        emails {
          value
          primary
        }
      }
  	}
  }
}

During querying, intellisense features are available to explain the meaning of each object and field:

Graphiql Intellisense

To make data changing commands you can use GraphQL mutations. When creating a user account you can optionally also set a password:

mutation createAccount
{
  createAccount(input: {
    fields: {
      userName: "john.doe",
      password: "Password1",
      emails: {
        value: "john.doe@mycompany.com",
        primary: true
	  }
    }
  }) {
    account {
      userName
      emails {
        value
      }
    }
  }
}

The following example shows an update operation for a user account whose name has changed, so that the userName, displayName and email fields are all set to new values:

mutation updateAccount
{
  updateAccountById(input: {
    accountId: "fbea947a-aabc-11ec-adcb-0242ac170002",
    fields: {
      userName: "jane.smith"
      displayName: "jane.smith"
      emails: {
        value: "jane.smith@mycompany.com"
        primary: true
      }
    }
  }) {
    account {
      displayName
      emails {
        value
      }
    }
  }
}

Finally, user accounts can be deleted with the following syntax:

mutation deleteAccount
{
  deleteAccountById(input: {
    accountId: "fbea947a-aabc-11ec-adcb-0242ac170002"
  }) {
    deleted
  }
}

There are potentially many other GraphQL features you can use, to put you in better control of your identity data, though the above examples will enable you to get connected.

Custom User Fields

Starting in version 7.2 of the Curity Identity Server, GraphQL can be used to save custom fields against user accounts. First you need to define custom fields in the Admin UI under User Management Service / General. The following example extends the GraphQL schema to add a string field and a numeric field:

Graphql Custom Fields

You can then run a mutation of the following form to populate the custom fields for a user and save them to the user account data. This example query also echoes back the values in the mutation response.

mutation updateAccount
{
  updateAccountById(input: {
    accountId: "4d807914-ab51-11ec-b593-0242ac170002",
    fields: {
      Custom1: "A string value"
      Custom2: 123
    }
  }) {
    account {
      Custom1
      Custom2
    }
  }
}

The custom values are then saved within the attributes column of the data row for the user account, and can then be retrieved in the same way as any other GraphQL field. The values can then be used during authentication and token issuing. The Working with Claims tutorial describes how to include custom values from user account data in access tokens.

Users and Dynamic Clients

In some security use cases, Dynamic Client Registration (DCR) is used to register one or more distinct OAuth clients per user. This technique is described in Mobile Best Practices, where a separate dynamic client is registered per user and device. The Authenticated DCR mobile code example provides a working end-to-end solution.

From version 7.2 of the Curity Identity Server, dynamic clients mapped to users can also be accessed using GraphQL. First enable access to the additional data under User Management Service / General:

Graphql DCR Data

You can then use GraphQL to read details from the dynamically_registered_clients database table. The result set could potentially contain multiple dynamic clients per user, and the DCR fields are described in the Using Dynamic Client Registration article.

query findAccount
{
  accountByEmail(email: "demo@user.com") {
    id
    name {
      givenName
      familyName
    }
    emails {
      value
    }
    dynamicClients {
      client_id
      scope
    }
  }
}

All of this data can then be accessed for any user with an account record. As for other areas of the schema, GraphQL tooling will enable you to easily see which DCR fields are available:

GraphQL Advanced Query

Programmatic Clients

Once the above data access techniques are understood, you can begin GraphQL coding against user accounts in your own apps. There are many client libraries available to choose from, in all of the main technology stacks. These provide similar features to the GraphiQL tool, to enable productive development.

Conclusion

User accounts in the Curity Identity Server are easy to manage in your own apps. GraphQL endpoints provide a friendly API with modern tooling for developers, though SCIM endpoints are also available, and may be a better choice for some types of operation. To use GraphQL you must connect securely, with an access token that has the expected groups scope, and which maps to an Authorization Manager, where the level of access is defined.