Dynamically set access token expiry in Identity Cloud

Mark Nienaber
6 min readFeb 9, 2024

PingOne Advanced Identity Cloud (Formally ForgeRock Identity Cloud) provides the ability to customise the access token expiry time through simple JavaScript

This was not always the case!

Back in ancient times, setting access token expiry time was a static affair. The value was configured in the Authorization Server Provider settings, or on the OAuth Client settings, and that was the value you got. Strategies to get access tokens with different expiry times included using different clients to get access tokens of different expiry times.

Not anymore!

This article will describe the steps to achieve this use case:

As a client I need to get an access token on behalf of a user. The length of expiry of the token should based on the level of authentication. i.e. If the user authenticates with MFA — Token Expiry 10 minutes, If the user authenticates without MFA — Token Expiry 1 minutes

We will be using the Authorization Code Grant type as described here allowing use to get an Access Token.

Let’s get this set up!

Configure Journeys

LoginBasic

This simple journey, as seen below, does not set Auth Level so by default the Auth Level will be 0.

Basic Login Journey

LoginMFA

This journey will include a node to add + 100 to the Auth Level. After successfully traversing the journey you will have a session with an Auth Level of 100. In your environment this will likely be more complex with MFA nodes, however for simplicity we will just ensure the Auth Level is set correctly.

Create Access Token Modification Script

This script will be set on the client and run on token minting. This script will set the token expiry based on the Auth Level of the Authentication Session.

In the Platform UI go to Scripts > Auth Scripts, then Duplicate the Alpha OAuth2 Access Token Modification Script:

Give the Script a name that’s relevant, in this example it’s Dynamic expiry OAuth2 Access Token Modification Script.

Now let’s add in the relevant script

(function () {
if(accessToken.getField("auth_level")<99){
//logger.error('>>>>>>>. AUTHLEVEL = < 99 ' + session.getAuthLevel());
var currentDate = new Date();
// Add one minute (60 seconds) to the current time
var oneMinuteLater = new Date(currentDate.getTime() + 60000);
// Get the Unix timestamp (seconds since January 1, 1970)
var unixTimestampOneMinuteLater = Math.floor(oneMinuteLater.getTime() / 1000);
//logger.error('>>>>>>>. currentDate=' + currentDate);
//logger.error('>>>>>>>. oneMinuteLater=' + oneMinuteLater);
//logger.error('>>>>>>>. unixTimestampOneMinuteLater=' + unixTimestampOneMinuteLater);
accessToken.setField("exp", unixTimestampOneMinuteLater);
}
}());

You’ll note above that if the auth level less than 99 then the expiry time will be set to 1 minute.

This is done using the ‘exp’ field as noted in RFC7519

The “exp” (expiration time) claim identifies the expiration time on
or after which the JWT MUST NOT be accepted for processing. The
processing of the “exp” claim requires that the current date/time
MUST be before the expiration date/time listed in the “exp” claim.

Create OAuth Application

You can do this manually or via REST. Just make sure you the client has the following settings:

  • Grant Type = authorization_code
  • Token Endpoint Authentication=client_secret_post
  • Allow Implied Consent = true

All other settings are up to you. Sample REST call:

curl --location 'https://URL/am/json/realms/alpha/agents/?_action=create' \
--header 'Content-Type: application/json' \
--header '<cookie_name>: <Admin SSO Session>' \
--header 'Accept-API-Version: resource=3.0, protocol=1.0' \
--data '{
"username": "myClient",
"userpassword": "password",
"realm": "/",
"AgentType": ["OAuth2Client"],
"com.forgerock.openam.oauth2provider.jwtTokenLifeTime": "60",
"com.forgerock.openam.oauth2provider.grantTypes": [
"[1]=password",
"[4]=client_credentials",
"[0]=authorization_code",
"[3]=implicit",
"[2]=refresh_token"
],
"com.forgerock.openam.oauth2provider.scopes": [
"[0]=email",
"[1]=openid",
"[2]=profile",
"[2]=myemail",
"[2]=privileged"
],
"com.forgerock.openam.oauth2provider.tokenEndPointAuthMethod": [
"client_secret_post"
],
"com.forgerock.openam.oauth2provider.redirectionURIs": [
"[0]=http://www.google.com"
],
"com.forgerock.openam.oauth2provider.requestObjectSigningAlg": [
"HS256"
],
"isConsentImplied": [
"true"
],
"com.forgerock.openam.oauth2provider.claims": [
"[0]=email",
"[1]=profile"
]
}'

Attach the Custom Script to OAuth Application

Let’s set the overrides on the OAuth Application so it uses our custom script.

Browse to the Native AM console. Native Consoles > Access Management, then browse to Applications > OAuth 2.0 > Clients > OAuth2 Provider Overrides.

Ensure the following is set:

  • Enable OAuth2 Provider Overrides = TRUE
  • Access Token Modification Plugin Type = SCRIPTED
  • Access Token Modification Script = Dynamic expiry OAuth2 Access Token Modification Script (or whatever you called your script)
  • Allow Clients to Skip Consent = TRUE (I’m using Postman for testing so I’ll skip this)
  • Use Client-Side Access & Refresh Tokens = TRUE (Note: without this value set, the introspect endpoint doesn’t return the “expires_in” value which makes this hard to verify/test)

Testing

Let’s Check the results:

Privileged Session.

Expectation: Auth Level will be 100, and the expiry to be default i.e in this environment it’s 3600 seconds

Authenticate to LoginMFA Journey to get user

curl --location --request POST 'https://URL/am/json/alpha/authenticate?authIndexType=service&authIndexValue=LoginMFA' \
--header 'Content-Type: application/json' \
--header 'Accept-API-Version: resource=2.1'

Request Access token from Authorize endpoint passing in the authenticated session from above as a Cookie

curl --location 'https://URL/am/oauth2/realms/alpha/authorize?response_type=code&client_id=myClient&redirect_uri=http%3A%2F%2Fwww.google.com&state=abc123&scope=profile' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: e3307ef392bc9ba=Felzsl2IWBytu2epAyUx7yYERgM.*AAJTSQACMDIAAlNLABxidWM5K0xaMVhac3RRWkRUY1UvdHViTHc4TWs9AAR0eXBlAANDVFMAAlMxAAIwMQ..*'

Swap the Code for a token

curl --location 'https://URL/am/oauth2/realms/alpha/access_token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=lL2WKHEd4Pl5Q11HomEC5XKGrBU' \
--data-urlencode 'client_id=myClient' \
--data-urlencode 'client_secret=password' \
--data-urlencode 'redirect_uri=http://www.google.com'

My tokens are returned

{
"access_token": "<token>",
..
"expires_in": 3599
}

And Decoded:

{
"sub": "e825ba66-045b-419b-b1b9-56d625190027",
"cts": "OAUTH2_STATELESS_GRANT",
"auth_level": 100,
"auditTrackingId": "cd24c749-c6cd-47fa-9d72-1e1891323c8d-2023959",
"subname": "e825ba66-045b-419b-b1b9-56d625190027",
"iss": "https://URL:443/am/oauth2/alpha",
"tokenName": "access_token",
"token_type": "Bearer",
"authGrantId": "P5OyhcKUEkt-43Nru-a98kk_9CE",
"aud": "myClient",
"nbf": 1707451669,
"grant_type": "authorization_code",
"scope": [
"profile"
],
"auth_time": 1707451362,
"realm": "/alpha",
"exp": 1707455269,
"iat": 1707451669,
"expires_in": 3600,
"jti": "-I7npoHsP7-QHNE92gVBa6bNhqw"
}

Let’s hit the introspect endpoint and check the results

curl - location 'https://URL/am/oauth2/realms/alpha/introspect' \
- header 'Content-Type: application/x-www-form-urlencoded' \
- data-urlencode 'token=HwaPIB8pdLfyiXOoryQHYivYgMo' \
- data-urlencode 'client_id=myClient' \
- data-urlencode 'client_secret=password'

The Response:

{
"active": true,
"scope": "profile",
"realm": "/alpha",
"client_id": "myClient",
"user_id": "e825ba66-045b-419b-b1b9-56d625190027",
"username": "e825ba66-045b-419b-b1b9-56d625190027",
"token_type": "Bearer",
"exp": 1707455269,
"sub": "e825ba66-045b-419b-b1b9-56d625190027",
"iss": "https://URL:443/am/oauth2/alpha",
"subname": "e825ba66-045b-419b-b1b9-56d625190027",
"auth_level": 100,
"authGrantId": "P5OyhcKUEkt-43Nru-a98kk_9CE",
"auditTrackingId": "cd24c749-c6cd-47fa-9d72-1e1891323c8d-2023959",
"expires_in": 3593
}

Notice that the “exp” time is set which affects the “expires_in” that’s correctly set to 3593

LoginBasic Journey

Expectation: Auth Level will be 0, so our expectation is the token expires in 1 minute / 60 seconds.

Authenticate to LoginBasic Journey to get user

curl --location --request POST 'https://URL/am/json/alpha/authenticate?authIndexType=service&authIndexValue=LoginBasic' \
--header 'Content-Type: application/json' \
--header 'Accept-API-Version: resource=2.1'

Request Access token from Authorize endpoint passing in the authenticated session from above as a Cookie

curl --location 'https://URL/am/oauth2/realms/alpha/authorize?response_type=code&client_id=myClient&redirect_uri=http%3A%2F%2Fwww.google.com&state=abc123&scope=profile' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: e3307ef392bc9ba=<SSOToken>'

Swap the Code for a token

curl --location 'https://URL/am/oauth2/realms/alpha/access_token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=lL2WKHEd4Pl5Q11HomEC5XKGrBU' \
--data-urlencode 'client_id=myClient' \
--data-urlencode 'client_secret=password' \
--data-urlencode 'redirect_uri=http://www.google.com'

My tokens are returned

{
"access_token": "<token>",
..
"expires_in": 59
}

Let’s hit the introspect endpoint and check the results

curl - location 'https://URL/am/oauth2/realms/alpha/introspect' \
- header 'Content-Type: application/x-www-form-urlencoded' \
- data-urlencode 'token=<Token>' \
- data-urlencode 'client_id=myClient' \
- data-urlencode 'client_secret=password'

The response :

{
"active": true,
"scope": "profile",
"realm": "/alpha",
"client_id": "myClient",
"user_id": "e825ba66-045b-419b-b1b9-56d625190027",
"username": "e825ba66-045b-419b-b1b9-56d625190027",
"token_type": "Bearer",
"exp": 1707455073,
"sub": "e825ba66-045b-419b-b1b9-56d625190027",
"iss": "https://URL:443/am/oauth2/alpha",
"subname": "e825ba66-045b-419b-b1b9-56d625190027",
"auth_level": 0,
"authGrantId": "NyiFowJN2Svw2_z__YnKyc6HdMA",
"auditTrackingId": "cd24c749-c6cd-47fa-9d72-1e1891323c8d-2075790",
"expires_in": 45
}

Notice that the “exp” time is set which affects the “expires_in” that’s correctly set to 45 (counting down).

You can keep hitting the introspect endpoint until you see that it expires :

{
"active": false
}

And that’s it, you have a script that dynamically sets the token expiry based on the Auth Level.

--

--