Skip to content

Add OAuth2 Access Token Modification Script (OAUTH2_ACCESS_TOKEN_MODIFICATION)#1034

Open
vharseko wants to merge 4 commits into
OpenIdentityPlatform:masterfrom
vharseko:issue/acr-amr-2
Open

Add OAuth2 Access Token Modification Script (OAUTH2_ACCESS_TOKEN_MODIFICATION)#1034
vharseko wants to merge 4 commits into
OpenIdentityPlatform:masterfrom
vharseko:issue/acr-amr-2

Conversation

@vharseko
Copy link
Copy Markdown
Member

@vharseko vharseko commented Jun 3, 2026

Summary

This PR introduces a fully-fledged OAuth2 Access Token Modification script type
(script context OAUTH2_ACCESS_TOKEN_MODIFICATION), analogous to ForgeRock AM 6.5.

It adds a supported extension point that allows custom claims (e.g. acr,
amr, roles, profile attributes) to be embedded directly into stateless (JWT)
OAuth 2.0 access tokens and refresh tokens
at issue time — without an extra
call to /oauth2/tokeninfo.

Closes / addresses: OpenAM Discussion #1027

Background / Problem

The stateless JWT claim set is built in
StatelessTokenStore.createAccessToken(...)
directly through JwtClaimsSetBuilder with a hard-coded set of fields and no
extension point
.

As a result, by design:

  • The OIDC Claims Script, custom Scope Implementation Class, and
    scope-to-claim mappings are never invoked when issuing a stateless JWT
    access token (they only apply to id_token, /userinfo, /tokeninfo).
  • Unlike commercial ForgeRock AM 6.5+, OpenIdentityPlatform/OpenAM had no
    Access Token Modification Script
    .

Users therefore could not get acr/amr/custom claims into the access token
payload without an additional /tokeninfo round-trip.

Solution

A new script context OAUTH2_ACCESS_TOKEN_MODIFICATION is executed when a
stateless access token or refresh token is issued. Claims the script adds,
modifies, or removes are merged into the token payload before signing.

The script runs in both createAccessToken and createRefreshToken, so
custom claims survive the refresh cycle.

Script bindings

Binding Type Description
accessToken ScriptableAccessToken Mutable view of the token: setField / getField / removeField
scopes Set<String> Granted scopes
identity AMIdentity (nullable) Resource owner identity
session SSOToken (nullable) User session if present
requestProperties Map<String, Object> Selected request properties (clientId, realm, grantType)
logger Debug OAuth2Provider debug logger

Default script (Groovy)

A safe default global script is shipped (UUID
d22f9a0c-426a-4466-b95e-d0f125b0d5fa). It propagates acr/amr and includes
commented examples (e.g. adding email). When the provider setting is
[Empty], the token is issued unchanged.

Changes

Scripting infrastructure (openam-scripting)

  • ScriptConstants: new ScriptContext.OAUTH2_ACCESS_TOKEN_MODIFICATION,
    *_NAME constant, GlobalScript entry.
  • ScriptingGuiceModule: StandardScriptEngineManager + named ScriptEvaluator.
  • ScriptEngineConfigurator: register engine config for the new context.
  • ScriptConfigurationService: usage-count handling for the new context.
  • scripting.xml / scripting.properties: choice values, context sub-config
    with class whitelist, default global Groovy script.
  • groovy/access-token-modification.groovy: default script + pom.xml inject.

OAuth2 (openam-oauth2 / openam-core)

  • OAuth2Constants: provider setting
    forgerock-oauth2-provider-access-token-modification-script, new
    ScriptParams (accessToken, requestProperties).
  • ScriptableAccessToken (new): mutable token view exposed to scripts.
  • OAuth2AccessTokenModifier (new): loads & evaluates the script, builds
    bindings, returns claims to merge.
  • StatelessTokenStore: invokes the modifier and merges claims in both
    createAccessToken and createRefreshToken.
  • OAuth2GuiceModule: threads the new dependency into the realm-agnostic store.
  • OAuth2Provider.xml / .properties / .section.properties: new provider
    attribute with scriptSelect UI type.

Upgrade / defaults

  • OAuth2ProviderUpgradeHelper: registers the new attribute for upgrades.
  • serviceDefaultValues.properties: GlobalAccessTokenModificationScriptId.

Documentation (openam-documentation)

  • Dev Guide: chap-scripting.adoc (new API section + context list),
    chap-client-dev.adoc (language table + context values).
  • Reference: chap-config-ref.adoc (provider attribute + scripting context).
  • Admin Guide: chap-manage-scripts.adoc (context enum).

Tests

  • StatelessTokenStoreTest: +4 tests (claims merged into access token & refresh
    token; empty result leaves token unchanged; constructor update).
  • ScriptableAccessTokenTest (new): 6 tests for the binding object API.

UI impact

None required. Script contexts and the OAuth2 Provider attribute are rendered
dynamically from the service schema (ScriptsService.getAllContexts() /
getContextSchema(), and the scriptSelect attribute via ScriptChoiceValues).
OIDC_CLAIMS is likewise never hard-coded in the UI.

Backwards compatibility

Fully backwards compatible. When no script is selected ([Empty]), the token is
issued exactly as before. The feature only applies to stateless (JWT) tokens.

How to use

  1. Realms → Realm → Services → OAuth2 Provider → Core.
  2. Enable Use Stateless Access & Refresh Tokens.
  3. Set OAuth2 Access Token Modification Script (default:
    OAuth2 Access Token Modification Script), or edit the global script under
    Realms → Realm → Scripts.

Testing

mvn -o -pl openam-oauth2 test -Dtest=StatelessTokenStoreTest,ScriptableAccessTokenTest
# Tests run: 15, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS

Affected modules also compile cleanly:

mvn -o -pl openam-core,openam-scripting,openam-oauth2,openam-upgrade -am compile -DskipTests

vharseko added 2 commits June 3, 2026 18:37
Propagate the authentication context class reference (`acr`) and the
authentication modules (`authModules`, i.e. `amr`) into the stateless JWT
access token, mirroring the behaviour already present in
`StatelessTokenStore.createRefreshToken`.

Previously these claims were only emitted into the `id_token`, the
`/oauth2/userinfo` and `/oauth2/tokeninfo` responses. The stateless access
token was built from a hard-coded claim set with no extension point, so PEPs /
API gateways had to make an extra `/oauth2/tokeninfo` round-trip to read
`acr`/`amr`. They can now be read directly from the access token payload.

The values are sourced from:
- the `AuthorizationCode` (authorization_code grant), and
- the previous `RefreshToken` (refresh_token grant), which overrides the
  authorization-code values when present.

Both claims are added only when a value is available, so the change is
additive and backward-compatible (e.g. client_credentials grant emits neither).

Changes:
- openam-oauth2: StatelessTokenStore.createAccessToken now extracts and adds
  the `acr` and `authModules` claims.
- openam-oauth2: StatelessTokenStoreTest adds 4 tests covering
  authorization-code source, refresh-token source, refresh-token precedence,
  and the no-source case.
- docs: chap-openid-connect.adoc updates the decoded stateless access token
  example and adds a note explaining when `acr`/`authModules` appear.

Refs: OpenAM Discussion OpenIdentityPlatform#1027
…ateless JWT

Add a fully-fledged OAuth2 Access Token Modification script (script context
`OAUTH2_ACCESS_TOKEN_MODIFICATION`), analogous to ForgeRock AM 6.5, so that
custom claims (e.g. `acr`, `amr`, roles, profile attributes) can be embedded
directly into stateless (JWT) access and refresh tokens at issue time —
without an extra `/oauth2/tokeninfo` round-trip.

Previously the stateless JWT claim set in StatelessTokenStore was hard-coded
with no extension point, so the OIDC Claims Script / custom Scope
Implementation Class were never invoked when issuing access tokens.

Script bindings: `accessToken` (mutable, setField/getField/removeField),
`scopes`, `identity`, `session`, `requestProperties`, `logger`.

The script runs both in createAccessToken and createRefreshToken so custom
claims survive the refresh cycle.

Changes:
- ScriptConstants: new ScriptContext + GlobalScript (uuid
  d22f9a0c-426a-4466-b95e-d0f125b0d5fa)
- ScriptingGuiceModule: engine manager + named ScriptEvaluator
- ScriptEngineConfigurator / ScriptConfigurationService: register + usage count
- scripting.xml / scripting.properties / ScriptResource.properties: context,
  whitelist, default global Groovy script, i18n
- groovy/access-token-modification.groovy: default script
- OAuth2Constants: provider attribute + ScriptParams (accessToken,
  requestProperties)
- ScriptableAccessToken: mutable token view exposed to scripts (new)
- OAuth2AccessTokenModifier: loads & evaluates the script, merges claims (new)
- StatelessTokenStore + OAuth2GuiceModule: invoke modifier for access & refresh
- OAuth2Provider.xml/.properties/.section.properties: new provider setting
- OAuth2ProviderUpgradeHelper + serviceDefaultValues.properties: upgrade/default
- docs: dev-guide (chap-scripting, chap-client-dev), reference (chap-config-ref),
  admin-guide (chap-manage-scripts)
- tests: StatelessTokenStoreTest (+4), ScriptableAccessTokenTest (new)

No UI changes required: script contexts and the provider attribute are rendered
dynamically from the service schema.

Backwards compatible: when no script is selected (`[Empty]`) the token is issued
unchanged.

Refs: OpenAM Discussion OpenIdentityPlatform#1027
@vharseko vharseko requested a review from maximthomas June 3, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant