By Mark Nienaber
Originally posted on marknienaber.medium.com
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.