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.
context
All Token procedures have the following structure:
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.
Note
Reference documentation for each kind of token procedure can be found at Procedure Context object.
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.
Assisted Token Flow Types
Authorization Endpoint Flow Types
Token Endpoint Flow Types
Introspection Endpoint Flow Types
UserInfo Endpoint Flow Types
Device Authorization Flow Types
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.
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.
$IDSVR_HOME/etc/init/token-procedures
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.
tokenContext
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.
For OAuth 2.0 Token Exchange the context needs to be initialized before issuing tokens. See Context Initialization for more details.
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.
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.
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().
getDefaultDelegationData()
1
var delegationData = context.getDefaultDelegationData();
The following delegation properties are provided by the context.
issued
revoked
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.
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.
delegationIssuer
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.
delegation
id
clientId
consent
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.
getDefaultAccessTokenData()
Note that a delegation is required for an Access Token to be issued.
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 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).
getDefaultRefreshTokenData()
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.
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:
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.
issuedRefreshToken
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.
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.
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.
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;
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
The main function of a token procedure
oauthTokenProcedureContext
A Map containing the result parameters to pass to the requesting client.
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.
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.
user
Footnotes