Token procedures

The OAuth Authorization Server uses token procedures, which are JavaScript scripts, to issue tokens (see Issuing OAuth and OpenId Connect Tokens). There are a number of different types of Token Procedures, all with the same method signature, but they receive different context object depending on which endpoint they are designed to serve.

All Token procedures have the following structure:

Listing 261 Token Procedure signature
1
2
3
4
5
6
7
8
function result(tokenContext) {

    // use the context to issue token(s)

    // populate and return the response model
    var responseData = {};
    return responseData;
}

There are a number of different kinds of token procedures. Each one represents a full or partial OAuth or OpenID Connect flow. The flow type determines what will be available on the context object when the procedure is called. When configuring a new token procedure, the admin should set the flow type that it can be used for, which will make it show up in the appropriate place on the endpoints.

Endpoint Kind Description Available Flows
oauth-assisted-token Assisted Token Endpoint
  • assisted-token-endpoint-identity
oauth-authorize OAuth Authorize Endpoint
  • oauth-authorize-authorization-code
  • oauth-authorize-implicit
  • openid-authorize-hybrid
oauth-introspect OAuth Introspection endpoint
  • oauth-introspect
  • oauth-introspect-application-jwt
oauth-token OAuth Token Endpoint
  • oauth-token-authorization-code
  • oauth-token-client-credentials
  • oauth-token-refresh
  • oauth-token-resource-owner-password-credentials
  • oauth-token-token-exchange
  • oauth-token-device-code
  • oauth-token-assertion
oauth-userinfo OpenID Connect Userinfo Endpoint
  • openid-userinfo
oauth-device-authorization OAuth Device Authorization endpoint
  • oauth-device-authorization

Assisted Token Flow Types

  • assisted-token-endpoint-identity - Called for the assisted token type on the Assisted Token endpoint

Authorization Endpoint Flow Types

  • oauth-authorize-authorization-code - Called for the ‘authorization_code’ response type on the Authorize endpoint
  • oauth-authorize-implicit - Called for the ‘token’ response type on the Authorize endpoint
  • openid-authorize-hybrid - Called for the OpenID Connect Hybrid Flow response types on the Authorize endpoint

Token Endpoint Flow Types

  • oauth-token-authorization-code - Called for the ‘authorization_code’ grant type on the Token endpoint
  • oauth-token-client-credentials - Called for the ‘client_credentials’ grant type on the Token endpoint
  • oauth-token-refresh - Called for the ‘refresh_token’ grant type on the Token endpoint
  • oauth-token-resource-owner-password-credentials - Called for the ‘password’ grant type on the Token endpoint
  • oauth-token-token-exchange - Called for the ‘https://curity.se/grant/accesstoken’ grant type on the Token endpoint
  • oauth-token-device-code - Called for the ‘urn:ietf:params:oauth:grant-type:device_code’ grant type on the Token endpoint
  • oauth-token-assertion - Called for the ‘urn:ietf:params:oauth:grant-type:jwt-bearer’ grant type on the Token endpoint

Introspection Endpoint Flow Types

  • oauth-introspect - Called when introspecting a token on the Introspect endpoint
  • oauth-introspect-application-jwt - Called when introspecting a token on the Introspect endpoint (with application/jwt provided in the client’s Accept header)

UserInfo Endpoint Flow Types

  • openid-userinfo - Called when userinfo is requested on the Userinfo endpoint

Device Authorization Flow Types

  • oauth-device-authorization - Called when the UserCode is verified and the device_code is issued as nonce

The context on every token procedure is capable of providing a documented list of properties. You could write this to the log while developing custom procedures to see what’s available from the particular context your procedure is being provided with.

Listing 262 Example Token Procedure Context documentation
1
2
3
function result(context) {
  console.log(context.describe());
}

Examples of token procedures are delivered in the $IDSVR_HOME/etc/init/token-procedures directory. Each subdirectory represents a flow type as described above. By placing a JavaScript file in such directory will cause it to be added to the configuration when the system starts for the first time. The name of the file will be used as the id of the procedure in the configuration.

Issuing tokens

The purpose of most Token Procedures is to issue new tokens. These are OAuth and OpenID Connect tokens, such as Access Tokens, Refresh Tokens and ID Tokens. The reason for doing this in a procedure is that often it makes sense to customize how the token is structured, or issue more than one token, either inside another token, or next to another token.

The process of issuing tokens uses a concept of Token Issuers. These are defined in the configuration and by default a single token issuer of each kind is configured. However when issuing multiple tokens of different kinds custom issuers are also needed. These are simply named issuers, in contrast to the unnamed default issuer.

Token issuers are functions on the tokenContext that take a Map as input and return the compiled token as a string output.

Listing 263 Example of token issuance for the client_credentials grant type
 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)
  };
}

This procedure does exactly the same thing as what Curity would do if no procedure is configured. As you can see there are a lot of help provided when issuing tokens. The following sections break this down into details.

The details of the context can be found in the API reference for each endpoint type below. The example shows that the accessTokenIssuer needs two things. Input data for the token and a delegation.

Delegations

In Curity, the base of all tokens is a delegation. This represents the authorization that was granted by the user to this client. It’s an abstract concept that in itself is not a token. A delegation is the base for refresh tokens and access tokens, and the same delegation is commonly used for many tokens for the same user-client pair.

The process of issuing a token after a new OAuth grant has been presented typically follows these steps.

  1. Create a new delegation
  2. Issue a Refresh Token (if the OAuth flow supports it)
  3. Issue an Access Token
  4. Issue other tokens if needed

Depending on the flow used, this may be done in different places. But the rule of thumb is that the delegation is issued when it’s time to issue a new token.

Important

When a client refreshes a token, no delegation is issued. The same delegation is used.

This also means that if the delegation is revoked. All tokens issued for that delegation become revoked.

Issuing Delegations

To issue a delegation, an map with the properties of the delegation needs to be passed to the delegation issuer. A map with suggested properties of a delegation can be provided by the context’s method getDefaultDelegationData().

Listing 264 Initializing the default delegation data from the context
1
var delegationData = context.getDefaultDelegationData();

The following delegation properties are provided by the context.

name mandatory description
owner yes the name of the user that authenticated for the token
created yes the timestamp (seconds since epoch) that the delegation was created
scope yes the scope that was authorized by the subject
clientId yes the client_id that initiated the request
expires yes the timestamp (seconds since epoch) when the delegation will expire
redirectUri no if provided, the redirect_uri that was included in the initial request
status yes the status of the delegation, can be issued or revoked
authenticationAttributes yes the map with authentication attributes. More on that in another section.

It is possible to add your own custom properties to delegation data, so they are issued as part of the new delegation. To do that, you can treat delegation data as a standard JavaScript object, and work with its properties.

Listing 265 Setting custom properties in a delegation
1
2
var delegationData = context.getDefaultDelegationData();
delegationData.myproperty = 'zort';

The actual issuance of a delegation is done using the delegationIssuer from the context. Combining the above, the following snippet would issue a delegation with a custom property added to it.

Listing 266 Issuing a delegation
1
2
3
4
var delegationData = context.getDefaultDelegationData();
delegationData.myproperty = 'zort';

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

The operation of issuing a delegation returns a representation of this delegation, that should be used later when tying tokens to the delegation. The returned delegation object only has the following properties: id, clientId, and (optional) consent.

Issuing an Access Token

Issuing access tokens follows the same procedure as issuing the delegation. A map of input data to go into the token needs to be defined, and given together with the delegation to the accessTokenIssuer.

The context provides an initialized map of Access Token properties through the getDefaultAccessTokenData() method.

Note that a delegation is required for an Access Token to be issued.

Listing 267 Issuing an Access Token
1
2
3
var accessTokenData = context.getDefaultAccessTokenData();

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

The returned token is a String-based representation of the token’s properties.

Issuing a Refresh Token

Issuing Refresh Tokens is the same operation as issuing an Access Tokens, the delegation is needed and a map of input data.

The context provides an initialized map of Refresh Token properties through the getDefaultRefreshTokenData() method. It is possible to disable the issuing of Refresh Tokens, either for the profile or for the particular client making the request. In that case, the context will be initialized with this state, and when getDefaultRefreshTokenData() is called, it will be empty (to be exact, it will be null).

The result from the call to getDefaultRefreshTokenData() can be provided to the Refresh Token issuer. If a RefreshToken could be issued, its value is returned. Otherwise, null will be the result of issuing a Refresh Token. Please take a look at the following example to see how this works.

Listing 268 Issuing a Refresh Token
1
2
var refreshTokenData = context.getDefaultRefreshTokenData();
var issuedRefreshToken = context.refreshTokenIssuer.issue(refreshTokenData, delegation);

This issuance differs from the others in a few ways. It is possible to configure a client to not allow refresh tokens to be issued. This is handled gracefully in the procedures via nullability. If the client’s configuration has disabled refresh token issuance the following will happen:

Listing 269 Issuing a Refresh Token
1
2
3
4
5
6
var refreshTokenData = context.getDefaultRefreshTokenData();
//refreshTokenData === null is true at this point

//it is safe to pass null to the issuer
var issuedRefreshToken = context.refreshTokenIssuer.issue(refreshTokenData, delegation);
//issuedRefreshToken is also null at this point

So with the issuedRefreshToken being null, the procedure doesn’t need to worry about the configuration of the client. It can safely add the nullable token to the response and Curity will filter null values out before producing the JSON data.

Listing 270 Issuing a Refresh Token
1
2
3
4
5
...
var result = {
  ...
  'refresh_token' : issuedRefreshToken
}

This is perfectly safe, even if issuedRefreshToken is null.

Note that a delegation is required for a Refresh Token to be issued.

Note

Not all OAuth flows use Refresh Tokens. If changing this behaviour the flow may become incompatible with the OAuth specification. See RFC 6749 for more details.

Preparing the response

Once the procedure has created the tokens it needs, it’s time to prepare a response. This is used when the server generates the response to the client. There are certain values that are expected to be in this response value, all according to each flow in RFC 6749.

When the procedure has issued a RefreshToken, it must be part of the returned response data. When the RefreshToken could not be issued, the issuer will return with a null-value. It is safe to add this to the response data though, as the server will filter out all entries with a null-value before sending it to the requesting client.

Listing 271 Generating a response
1
2
3
4
5
6
7
8
9
var responseData = {
  scope: accessTokenData.scope,
  access_token: issuedAccessToken,
  refresh_token: issuedRefreshToken,
  token_type: 'bearer',
  expires_in: secondsUntil(accessTokenData.exp)
};

return responseData;

Token Procedure Function Signature

All token procedures have the following function signature. The result function takes one argument: context, which contains information about the current operation to be performed.

result(context)

The main function of a token procedure

Arguments:
  • context – The oauthTokenProcedureContext context object for token procedures.
Returns:

A Map containing the result parameters to pass to the requesting client.

Return Value

The returned Map should at least contain the parameters expected by the current OAuth flow according to RFC 6749. The provided procedures in $IDSVR_HOME/etc/init/token-procedures show a good baseline of what should be responded with. It is allowed to add additional parameters to the response, however it is not recommended to remove parameters.

Including Request Parameters Values

It is possible with many token procedure contexts to get access to query and form parameter values. It is also possible to get HTTP header values. These are tainted values that should only be used after verification. If limited or no verification is performed, it is important to realize that the caller is in a trusted subsystem together with the Curity Security Token Server. In such deployments, if the parameter values are included in tokens and/or the response, it is advisable that the caller verifies that the parameters were included as expected. This control measure is prudent, as the consequences of any unforeseen mix-ups or errors can be far reaching. As an example of how to do this, consider this token issuance script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  function result(context) {
      var delegationData = context.getDefaultDelegationData();
      var issuedDelegation = context.delegationIssuer.issue(delegationData);
      var user = context.request.getParameterValueOrError("user");

      var accessTokenData = context.getDefaultAccessTokenData();
      accessTokenData["user"] = user;
      var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, issuedDelegation);

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

Here, the caller is providing a user request parameter. The Curity Security Token Server has no idea where this value came from and it cannot be verified. The example procedure, however, places the value in the issued token (line 7) and the response (line 12). Since the value is assigned to a variable (line 4) and that is used when placing the data into the token and the response, the caller may simply verify the response data or it can be more paranoid and check the token as well. Not checking either may be OK if the asserted value is not used for critical situation. If it is highly sensitive data, however, it should be verified before forwarding or using.