Dynamically set access token expiry in Identity Cloud
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.
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.