Skip to content

Update Token

Overview#

By overriding the interface methods in UpdateTokenType inside a custom script you can

  1. Enable transformation of claims and values in id_token and Access token e.g. add a custom claim to an id_token, change a token expiry, change the sub value, or remove the nonce.

Example use-case:
* As per the open banking standard, the id_token should contain claim openbanking_intent_id and the same value should also reflect in the sub claim. * As specified in the FAPI Baseline Specification the sub claim should have the user id.

  1. Set a specific token lifetime

  2. Perform extra business logic like adding or removing scopes.

  3. Add an extra audit log for each token response.

Flow#


sequenceDiagram
title UpdateToken script
autonumber 1
RP->>Jans AS: Request token
Jans AS->>Jans AS: Is UpdateToken script is associated with client? <br/>(or is there a script that applies to all clients? )
Jans AS->>Jans AS: do stuff
note  right of Jans AS: do stuff<br/>1. Enable transformation of claims and values in id_token <br/> OR 2. set token expiry <br/> or 3. add / remove scopes. <br/> or 4. perform audit logs
Jans AS->>RP: return token(s) (Access token, ID token or Refresh Token) reflecting step 3

Adding the custom script to Jans server#

  1. Create cs.json with the contents of a CUSTOM script. To do that, run the following command.
/opt/jans/jans-cli/config-cli.py --schema CustomScript > /tmp/cs.json
  1. Edit the file's contents to reflect the addition of the UpdateToken custom script.

  2. Set enabled flag true

  3. Configure any parameters that the script may use.
  4. name field should reflect the use case
  5. script_type should be UPDATE_TOKEN
  6. script.py can have contents similar to Sample Script) and is present in jans-cli's host machine.
{
  "dn": null,
  "inum": null,
  "name": "update_token",
  "aliases": [],
  "description": "Update token custom script",
  "script": "_file /root/script.py",
  "scriptType": "UPDATE_TOKEN",
  "programmingLanguage": "JYTHON",
  "moduleProperties": {
    "value1": null,
    "value2": null,
    "description": null
  },
  "configurationProperties":

[
{
    "value1": "param_name",
    "value2": "DI3ICTTJKLL8PPPNGH7YI",
    "description": "This is just an example",
    "hide": true
  },
  {
    "value1": "param_name_2",
    "value2": "eEbJdi3hg42zxyFYbHArU5RuioPP",
    "description": "yet another example",
    "hide": true
  }
]
,
  "level": "integer",
  "revision": 0,
  "enabled": true,
  "scriptError": {
    "raisedAt": null,
    "stackTrace": null
  },
  "modified": false,
  "internal": false
}
  • Add the custom script. Save the response, it will contain the inum of the newly added script.
  • /opt/jans/jans-cli/config-cli.py --operation-id post-config-scripts --data /tmp/cs.json
    

Associate an Update Token script to a client (RP) [optional step]#

📝 Note: If the Update token script is not associated with a client, then it will be applicable to all clients registered in the Jans Server. Which implies that all tokens obtained using the Jans server will reflect modifications as per the script.
To Associate an Update Token script to a client (RP), execute the command below with appropriate values for: - inum of the client - inum of the update_token script

/opt/jans/jans-cli/config-cli.py --operation-id patch-oauth-openid-clients-by-inum --url-suffix inum:inum_of_client --data '[ { "op": "add", "path": "updateTokenScriptDns", "value": ["inum={SCRIPT_ID},ou=scripts,o=jans"] } ]'

Writing an Update token script (Pseudo code for potential usecases)#

Mandatory methods#

class UpdateToken(UpdateTokenType):

    def __init__(self, currentTimeMillis):
        self.currentTimeMillis = currentTimeMillis

    def init(self, customScript, configurationAttributes):
        return True

    def destroy(self, configurationAttributes):
        return True

    def getApiVersion(self):
        return 11

modifyIdToken () : Used to modify claims in an ID token#

Pseudocode and example :

    # Returns boolean, true - indicates that script applied changes
    # jsonWebResponse - is JwtHeader, you can use any method to manipulate JWT
    # context is reference of io.jans.oxauth.service.external.context.ExternalUpdateTokenContext
    def modifyIdToken(self, jsonWebResponse, context):

        # header claims
    jsonWebResponse.getHeader().setClaim("header_name", "header_value")

    #custom claims
    jsonWebResponse.getClaims().setClaim("openbanking_intent_id", openbanking_intent_id_value)

    #regular claims        
    jsonWebResponse.getClaims().setClaim("sub", claimValue)

    return True

Inspecting a modified ID token

modifyAccessToken()#

Granularity of access control#

An UpdateTokenType script is great for adding scopes or removing scopes to/from the Access token. By doing so you can tailor build the granularity of access control according to business need.

context.overwriteAccessTokenScopes is ready to use method of the context variable

    def modifyAccessToken(self, accessToken, context):
              context.overwriteAccessTokenScopes(accessToken, Sets.newHashSet("openid", "mynewscope"))

Perform business check before returning AT#

Pseudocode and example - Issue Access token only if account balance is greater than 0

    # Returns boolean, true - indicates that script applied changes
    # accessToken - is JwtHeader, you can use any method to manipulate JWT
    # context is reference of io.jans.oxauth.service.external.context.ExternalUpdateTokenContext
    def modifyAccessToken(self, accessToken, context):

         #read from session
    sessionIdService = CdiUtil.bean(SessionIdService)
    sessionId = sessionIdService.getSessionByDn(context.getGrant().getSessionDn()) # fetch from persistence

        org_id = sessionId.getSessionAttributes().get("org_id")
    balance = thirdPartyApi.checkBalance(org_id)

        if balance > 0 :
           return True
        else:
           return False # forbid the creation of AT

Modify claims in an access token#

    # Returns boolean, true - indicates that script applied changes. If false is returned token will not be created.
    # accessToken is reference of io.jans.as.server.model.common.AccessToken (note authorization grant can be taken as context.getGrant())
    # context is reference of io.jans.oxauth.service.external.context.ExternalUpdateTokenContext
    def modifyAccessToken(self, accessToken, context):

        # header claims
        context.getHeader().setClaim("header_name", "header_value")

        #custom claims
        context.getClaims().setClaim("claim_name", "claimValue")

        #regular claims        
        context.getClaims().setClaim("sub", claimValue)

        return True

Modify a specific token lifetime based on the context#

  1. Refresh token lifetime:
    def getRefreshTokenLifetimeInSeconds(self, context):
        return 24 * 60 * 60 # one day
  1. ID token lifetime:
    def getIdTokenLifetimeInSeconds(self, context):
        return 10 * 60 * 60 # 10 hours
  1. Access token lifetime:

    def getAccessTokenLifetimeInSeconds(self, context):
        return 10 * 60 * 60 # 10 hours
Access token lifetime from script has highest priority (it wins from client's access token lifetime configuration).

modifyRefreshToken() :#

Used to modify claims in a Refresh Token

    # Returns boolean, true - indicates that script applied changes. If false is returned token will not be created.
    # refreshToken is reference of io.jans.as.server.model.common.RefreshToken (note authorization grant can be taken as context.getGrant())
    # context is reference of io.jans.as.server.service.external.context.ExternalUpdateTokenContext (in https://github.com/JanssenProject/jans-auth-server project, )
    def modifyRefreshToken(self, refreshToken, context):
        return True

IntrospectionType script vs UpdateTokenType script#

IntrospectionType UpdateTokenType
Client configuration parameter Introspection script is invoked only when accessTokenAsJwt=true Update token script is invoked irrespective of whether accessTokenAsJwt is true or false
Core Purpose Used to return access token meta information like current validity, approved scopes, and information about the context in which the token was issued when a Resource Server which queries the Introspection endpoint used to enable transformation of claims and values in id_token and Access token, set a specific token lifetime, change granularity of access control (up-scoping, down-scoping), audit logging for each token response, forbid the creation of AT based on a criteria.
Functionality 1. Can be used to modify claims of an Access token as JWT, however this it is recommended to use UpdateToken script instead. 1. Used to modify id_token, refresh token and access token
2. Introspection script cannot change scope of AT 2. UpdateToken can change scope of AT and modify AT object in persistence irrespective of the value of accessTokenAsJwt as true or false
Script Invocation sequence 2. After an Access token is generated 2. Before the creation of AT, id_token and refresh_token

Testing#

  1. Use this: Reference for testing
  2. Inspect the tokens. Use jwt.io to inspect the contents of a JWT.

FAQ#

  1. How can I add a dict type object as a claim value?

from io.jans.as.model.uti import JwtUtil
from org.json import JSONObject;

    def modifyIdToken(self, jsonWebResponse, context):
        datas =  {'country': 'ID', 'sponsor': '7022952467', 'role': 'BusinessOwner', 'salesplanaff': '220', 'acctsubtype': 'BusinessOwner', 'accttype': 'AmBCBusiness', 'abo': '7022953754', 'aboname': 'NEW', 'lclpartyid': '119700175', 'email': None, 'status': 'Active'}
        string_rep = json.dumps(datas)
        jsonObject = JwtUtil.fromJson(string_rep)
        jsonWebResponse.getClaims().setClaim("test", jsonObject)
        print "Update token script. Modify idToken: %s" % jsonWebResponse
        return True
2. How can I set integer value in id token claim?
jsonWebResponse.getClaims().setClaim("claimY", Integer.valueOf(124456191))
Get it as integer, via
getClaims().getClaimAsInteger("claimY")


Last update: 2024-12-10
Created: 2022-05-18