Token Procedure Examples

Overview

This chapter provides the procedure author with examples and information on how to build procedures for each flow. In the last section it also provides examples of common use-cases that many systems find themselves needing to provide.

Assisted Token Endpoint

Flow Type: assisted-token-endpoint-identity

The assisted token endpoint has one flow associated with it. To mimic the exact behaviour that occurs with the built-in procedure the following should be used.

Listing 272 Assisted Token Procedure - Default behaviour
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function result(context) {
  if (context.issuedToken) {
      var expiresIn = secondsUntil(context.issuedToken.expiresAt);

      return {
          status: "success",
          access_token: context.issuedToken.value,
          expires_in: expiresIn,
          scope: context.issuedToken.scope,
          subject: context.issuedToken.subject
      };
  }

  var delegationData = context.getDefaultDelegationData();
  var issuedDelegation = context.delegationIssuer.issue(delegationData);

  var accessTokenData = context.getDefaultAccessTokenData();
  var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, issuedDelegation);

  return {
      status: "success",
      access_token: issuedAccessToken,
      expires_in: secondsUntil(accessTokenData.exp),
      scope: accessTokenData.scope,
      subject: context.subjectAttributes().subject
  };
}

Line 2: As can be noted on line 2, we check if there is a currently issued token. If so, we re-use that token. The Assisted token endpoint supports re-use of tokens between sessions. This is so that long-lived sessions can be maintained in a single-page application where the user leaves and comes back. It’s of course possible to use the SSO session for this purpose as well, but configuring the oauth-server to re-use the assisted token adds flexibility.

Line 14: If no token was available, then a new needs to be issued. This process starts with issuing a delegation, using the default data provided from authentication, and then issuing the actual access token.

Line 20: In the response it’s possible to stick extra data if desired. This will be available to the JavaScript client in the application via the assistant.getAdditionalData() function on the assistant. See Assisted Token JavaScript API for details.

Authorize Endpoint

The assisted token endpoint has two flows associated with it. The first is the code flow that is the most common OAuth flow. The second is the implicit flow that issues tokens directly on the authorize endpoint.

Authorization Endpoint Code Flow

This flow continues on the Token endpoint Token Endpoint Code Flow when the first step is completed.

Flow Type: oauth-authorize-authorization-code

Listing 273 Authorize Endpoint Code Flow - Issue Authorization Code (Default Behaviour)
1
2
3
4
5
6
7
8
9
function result(context) {
  var authorizationCodeData = context.getDefaultAuthorizationCodeData();
  var issuedAuthorizationCode = context.authorizationCodeIssuer.issue(authorizationCodeData);

  return {
      code: issuedAuthorizationCode,
      state: context.providedState
  };
}

Line 7: The providedState refers the the state parameter that the client may have sent to the OAuth server in the request. If this was sent it must be provided in the response back to the client. It may be a null value, in which case Curity will remove it from the response before rendering it.

Without adding any specific behaviour to the procedure, it issues an Authorization Code and responds with the value of the code.

The AuthorizationCode that is issued will always contain the Authentication parameters that were established when the user authenticated. It is not necessary to add these explicitly to the code.

Authorization Endpoint Implicit Flow

Flow Type: oauth-authorize-implicit

Listing 274 Authorize Endpoint Implicit Flow - Issue Access Token (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function result(context) {
  var delegationData = context.getDefaultDelegationData();
  var issuedDelegation = context.delegationIssuer.issue(delegationData);

  var accessTokenData = context.getDefaultAccessTokenData();
  var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, issuedDelegation);

  var responseData = {
      access_token: issuedAccessToken,
      token_type: 'bearer',
      expires_in: secondsUntil(accessTokenData.exp),
      state: context.providedState
  };

  if (context.scopeNames.contains('openid')) {
      var idTokenData = context.getDefaultIdTokenData();
      responseData.id_token = context.idTokenIssuer.issue(idTokenData);
  }

  return responseData;
}

When issuing an Access token, in contrast to issuing an Authorization code, the author of the procedure must first issue a delegation. If no special data is required, the default delegation data contains all necessary information needed to issue a new delegation. The Authentication attributes established when the user authenticated are also automatically added to the delegation for later referencing.

Authorization Endpoint Hybrid Flow

Flow Type: openid-authorize-hybrid

Listing 275 Authorize Endpoint Hybrid Flow - Issue Tokens (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function result(context) {
    var authorizationCodeData = context.getDefaultAuthorizationCodeData();
    var issuedAuthorizationCode = context.authorizationCodeIssuer.issue(authorizationCodeData);

    var responseData = {
        code: issuedAuthorizationCode,
        state: context.providedState
    };

    var issuedAccessToken = null;

    var accessTokenData = context.getDefaultAccessTokenData();
    if (accessTokenData) {

        var delegationData = context.getDefaultDelegationData();
        var issuedDelegation = context.delegationIssuer.issue(delegationData);

        issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, issuedDelegation);

        responseData.access_token = issuedAccessToken
        responseData.token_type = 'bearer'
    }

    var idTokenData = context.getDefaultIdTokenData();
    if (idTokenData) {

        if (!idTokenData.nonce) {
            throw new TokenIssuerException("Missing required nonce.");
        }

        var idTokenIssuer = context.idTokenIssuer;
        idTokenData.c_hash = idTokenIssuer.cHash(issuedAuthorizationCode);
        idTokenData.at_hash = idTokenIssuer.atHash(issuedAccessToken);

        responseData.id_token = idTokenIssuer.issue(idTokenData);
    }

    return responseData;
}

Introspection Endpoint

The Introspection endpoint has one flow that can be configured. In normal cases the introspection won’t return any issued tokens, but rather the data tied to the token that is currently introspected.

Default behaviour

Flow Type: oauth-introspect

Listing 276 Introspection Endpoint - (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function result(context) {
    var responseData = {
        active: context.presentedToken.active
    };

    if (context.presentedToken.active) {
        appendObjectTo(context.presentedToken.data, responseData);
        responseData.token_type = context.presentedToken.type;
        responseData.client_id = context.presentedToken.delegation.clientId;
        responseData.expired_scope = context.presentedToken.expiredScopes;
    }

    return responseData;
}

Line 6: Does a simple check, if the token is active (valid) then return the data tied to the token, otherwise simply return the active status.

Phantom Token Behaviour

Flow Type: oauth-introspect-application-jwt and oauth-introspect

Commonly when introspecting a reference token (non-JWT), it is a gateway that performs the introspection. In order to reduce the number of calls to the introspection endpoint the endpoint can return an internal token (JWT or other) that can be used by the gateway to pass down to the API that should serve the call. This means that the Introspection procedure should use the original access token data to issue a duplicate token, a Phantom Token, and include that in the introspection response.

There are two types of procedures associated with phantom token issuance. The simplest one (and often enough to cover the most common scenarios) is the oauth-introspect-application-jwt flow. This is triggered by the client sending a regular introspection request with the Accept header set to a value of application/jwt. Given a valid reference token the response will be a phantom token (JWT) provided directly in the response body. In this flow, only the jwt and active properties of the response data are considered for rendering, and adding more has no effect on the end result. Should the active property be missing or set to anything other than true, or should the jwt property be missing, the server will respond with an empty response body (status code 204 No Content).

Listing 277 Introspection Endpoint - Phantom Token (application/jwt)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function result(context) {
    var responseData = {};
    var defaultAtJwtIssuer = context.getDefaultAccessTokenJwtIssuer();

    if (defaultAtJwtIssuer && context.presentedToken.active && context.delegation) {
        responseData.jwt = defaultAtJwtIssuer.issue(context.presentedToken.data, context.delegation);
        responseData.active = true;
    }

    return responseData;
}

Line 3: In order to issue a JWT, we check for the existence of a default configured JWT token issuer with context.getDefaultAccessTokenJwtIssuer() and store it in a variable.

Line 6: Given a configured JWT issuer and a valid token, issue a JWT and put it in the response data.

For client’s requiring more information than the “raw” JWT from the introspection, the application/json content type is preferable.

Listing 278 Introspection Endpoint - Phantom Token (application/json)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function result(context) {
    var responseData = {
        active: context.presentedToken.active
    };

    if (context.presentedToken.active) {
        var accessTokenData = context.presentedToken.data;
        var issuedDelegation = context.delegation;

        var issuedAccessToken = context.getAccessTokenIssuer("jwtAccessTokenIssuer").issue(accessTokenData, issuedDelegation);

        appendObjectTo(context.presentedToken.data, responseData);
        responseData.token_type = context.presentedToken.type;
        responseData.client_id = context.presentedToken.delegation.clientId;
        responseData.expired_scope = context.presentedToken.expiredScopes;
        responseData.phantom_token = issuedAccessToken;
    }

    return responseData;
}

Line 7: We copy the data from the presentedToken and store it in a variable

Line 10: Since we want to issue a different type of token, we use a different access token issuer than the default one, this is done my providing the configured ID of the access token issuer to the getter function.

Line 16: Finally include the JWT in the resulting data.

Phantom Token Behaviour using parameter trigger

As an alternative to the Phantom token behaviour it may be desirable to only provide the Phantom token when the introspecting client asks for it. There are many ways to do this, one is to simply have two introspect endpoints with different procedures attached (or no procedure on the default one).

Another way is to let the token procedure accept additional parameters in the request, that determine if it wants the Phantom token or not.

Listing 279 Introspection Endpoint - Phantom Token with trigger
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function result(context) {
    var responseData = {
        active: context.presentedToken.active
    };

    if (context.presentedToken.active) {
        appendObjectTo(context.presentedToken.data, responseData);
        responseData.token_type = context.presentedToken.type;
        responseData.client_id = context.presentedToken.delegation.clientId;
        responseData.expired_scope = context.presentedToken.expiredScopes;

        if (context.request.getFormParameter('phantom_token') === "true")
        {
          var accessTokenData = context.presentedToken.data;
          var issuedDelegation = context.delegation;
          var issuedAccessToken = context.getAccessTokenIssuer("jwtAccessTokenIssuer").issue(accessTokenData, issuedDelegation);
          responseData.phantom_token = issuedAccessToken;
        }
    }

    return responseData;
}

Line 12: We can use the request object on the context to access the incoming request.

Warning

Using parameters from the request object access raw input parameters, so checks need to be in place if returned to the client again. Refer to Including Request Parameters Values for details.

Token Endpoint

The Token Endpoint is the biggest endpoint in OAuth. It is overlaid with several flows, that are decided on which to use depending on the grant type that is provided. Some are stand-alone on the token endpoint only, while others like the code-flow starts at the Authorize endpoint and continues on the Token endpoint with a grant given from the Authorize endpoint.

Token Endpoint Code Flow

This flow starts on the Authorize endpoint with the Authorization Endpoint Code Flow. The Authorization code produced on that endpoint is presented here and is made available to the context as a presentedNonce if information from the .

Flow Type: oauth-token-authorization-code

Listing 280 Token Endpoint Code Flow - (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function result(context) {
  var delegationData = context.getDefaultDelegationData();
  var issuedDelegation = context.delegationIssuer.issue(delegationData);

  var accessTokenData = context.getDefaultAccessTokenData();
  var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, issuedDelegation);

  var refreshTokenData = context.getDefaultRefreshTokenData();
  var issuedRefreshToken = context.refreshTokenIssuer.issue(refreshTokenData, issuedDelegation);

  var responseData = {
      access_token: issuedAccessToken,
      refresh_token: issuedRefreshToken,
      token_type: 'bearer',
      expires_in: secondsUntil(accessTokenData.exp)
  };

  if (context.scopeNames.contains('openid')) {
      var idTokenData = context.getDefaultIdTokenData();
      responseData.id_token = context.idTokenIssuer.issue(idTokenData);
  }

  return responseData;
}

In this flow a refresh token is issued in addition to the Access Token and Delegation. If the openid scope is present, an Id token is also issued. As the reader notices there is no direct usage of the presentedNonce mentioned earlier. That is because all data needed from the incoming authorization code is already pre-populated in the default data objects used. However in cases where additional data was placed in the Authorization Code, the presentedNonce can be used to access that and pass it into the new tokens if needed.

Token Endpoint Client Credentials Flow

The simplest OAuth flow in the book is the client credentials flow. It does not involve a user, but simply issues a token on behalf of the client.

Flow Type: oauth-token-client-credentials

Listing 281 Token Endpoint Client Credentials Flow - (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function result(context) {
  var delegationData = context.getDefaultDelegationData();
  var issuedDelegation = context.delegationIssuer.issue(delegationData);

  var accessTokenData = context.getDefaultAccessTokenData();
  var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, issuedDelegation);

  return {
      scope: accessTokenData.scope,
      access_token: issuedAccessToken,
      token_type: 'bearer',
      expires_in: secondsUntil(accessTokenData.exp)
  };
}

The Client Credentials flow should not issue a refresh token, as the client simply can provide it’s credentials again to retrieve a new access token. It still needs to issue a delegation, as it is not possible to issue any access tokens without a delegation.

Token Endpoint Refresh Flow

Flow Type: oauth-token-refresh

Listing 282 Token Endpoint Refresh Flow - (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function result(context) {
  var accessTokenData = context.getDefaultAccessTokenData(context.delegation);
  var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, context.delegation);

  var refreshToken = context.presentedToken.value;

  if (refreshToken === null) {
      var refreshTokenData = context.getDefaultRefreshTokenData();
      refreshToken = context.refreshTokenIssuer.issue(refreshTokenData, context.delegation);
  }

  return {
      scope: accessTokenData.scope,
      access_token: issuedAccessToken,
      refresh_token: refreshToken,
      token_type: 'bearer',
      expires_in: secondsUntil(accessTokenData.exp)
  };
}

The refresh token flow is presented with a refresh Token as a presentedToken. This object contains information about the refresh token that the client sent to the server for refresh. Also available on the tokenProcedureContext is the delegation that was issued when the first refresh token was issued. That can be used to retrieve information related to the original issuance, for instance if the author stored session information or other information tied to the user, it can be accessed and used here.

Line 2: When default data for the new access token is requested, the delegation of the presented token is provided as a source of custom claim values. This ensures that the new access token contains the same claim values as the original token; i.e. no claims-value-provider will be requested to provide new claim values.

Line 5: The value on the presented token is available if the procedure is supposed to re-use the same token. This depends on the configuration of the client.

Line 7: If the value is null then a new token should be issued. Otherwise, re-use the already issued one.

Line 8: The refreshTokenData may also be null. It is possible to configure a client to not allow refresh tokens at all. If that is the case the default data returned is set to null.

Line 9: Even though the refreshTokenData may be null, it is safe to pass to the issue. The issuer will simply also return null if there was no input.

Line 15: In the response refreshToken may still be null. All null values are filtered out from the response before returning to the client, so it’s safe to include nullable values here.

Token Endpoint Resource Owner Password Credentials Flow

Very similar to the Token Endpoint Client Credentials Flow is the OAuth password grant flow. It passes the users credentials to the token endpoint for validation before the token is issued. As the reader notices these are not mentioned in the procedure. The authentication of the user has taken place before the procedure is executed, and therefore the procedure only needs to issue a token and a delegation.

Flow Type: oauth-token-resource-owner-password-credentials

Listing 283 Token Endpoint Resource Owner Password Credentials Flow - (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function result(context) {
  var delegationData = context.getDefaultDelegationData();
  var issuedDelegation = context.delegationIssuer.issue(delegationData);

  var accessTokenData = context.getDefaultAccessTokenData();
  var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, issuedDelegation);

  return {
      scope: accessTokenData.scope,
      access_token: issuedAccessToken,
      token_type: 'bearer',
      expires_in: secondsUntil(accessTokenData.exp)
  };
}

The difference is that the delegationData and the accessTokenData now contain information about the user. Typically the accessTokenData.sub is populated with the username given in the request.

Token Endpoint Token Exchange Flow

The Token Exchange flow is a special downgrade flow. It is used to downscope a token, remove audiences and scopes and issue a new token with the same expiration time. This is useful when passing tokens to systems outside of the own domain.

Flow Type: oauth-token-token-exchange

Listing 284 Token Endpoint Token Exchange Flow - (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function result(context) {
  var accessTokenData = context.getDefaultAccessTokenData(context.delegation);
  var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, context.delegation);

  return {
      scope: accessTokenData.scope,
      access_token: issuedAccessToken,
      token_type: 'bearer',
      expires_in: secondsUntil(accessTokenData.exp)
  };
}

No special action is needed. The default data already contains the downscoped details based on the request parameters. Notice how the existing delegation is used to issue the new token.

Line 2: When default data for the new access token is requested, the delegation of the presented token is provided as a source of custom claim values. This ensures that the new access token contains the same claim values as the original token; i.e. no claims-value-provider will be requested to provide new claim values.

Token Endpoint Assertion Flow

The Assertion Flow is used when the client uses an assertion as authorization grant. The assertion effectively authenticates the subject that requests the token. As such, all non-standardized claims inside the assertion are placed in the subject attributes of the Token Procedure’s context.

For example, when an assertion is presented with the following claims:

Listing 285 Example assertion claims
1
2
3
4
5
6
7
8
9
{
  "iss": "https://client-one",
  "sub": "teddie",
  "exp": 1528115231,
  "iat": 1528111631,
  "aud": "https://curity.example.com/oauth/token",
  "jti": "a89248fx-09-34812312",
  "shoesize": 44
}

Then the following procedure will include the custom shoesize attribute as a claim inside the issued Access Token:

Flow Type: oauth-token-assertion

Listing 286 Token Endpoint Assertion Flow - (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function result(context) {
  var delegationData = context.getDefaultDelegationData();
  var issuedDelegation = context.delegationIssuer.issue(delegationData);

  var accessTokenData = context.getDefaultAccessTokenData();
  accessTokenData.shoesize = context.subjectAttributes().shoesize;
  var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, issuedDelegation);

  return {
      scope: accessTokenData.scope,
      access_token: issuedAccessToken,
      token_type: 'bearer',
      expires_in: secondsUntil(accessTokenData.exp)
  };
}

UserInfo Endpoint

The UserInfo endpoint responds with user data tied to the account of the user owning the access token. A presentedToken is present with the information about the inbound token.

Flow Type: openid-userinfo

Listing 287 UserInfo Endpoint Flow - (Default Behaviour)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function result(context) {
  var responseData = context.getDefaultResponseData();

  if (context.accountAttributes['name'] && context.accountAttributes['name']['formatted']) {
      responseData.name = context.accountAttributes['name']['formatted'];
  }

  var presentedTokenData = context.presentedToken.data;

  responseData.scope = presentedTokenData.scope;
  responseData.client_id = context.presentedToken.delegation.clientId;
  responseData.preferred_username = context.accountAttributes.userName;

  return responseData;
}

In addition to the data about the token, a accountAttributes object is present on the context. This has been pre-populated with the account found in the defined account repository. This can be used to add additional details to the UserInfo response.

In the example above, the account is used to set the name attribute to the formatted version of the name attribute in the attributes.