Act As an OAuth 2.0 Resource Server
The following sections describe how IG acts as an OAuth 2.0 Resource Server, to resolve and validate access_tokens, and inject them into the context:
For information about allowing third-party applications to access users' resources without having users' credentials, see OAuth 2.0 Authorization Framework.
For information about the context, see OAuth2Context. For examples that use fields in OAuth2Context to throttle access to the sample application, see Configure Mapped Throttling and Configure Scriptable Throttling.
About IG As an OAuth 2.0 Resource Server
OAuth 2.0 includes the following entities:
-
Resource owner : A user who owns protected resources on a resource server. For example, a resource owner can store photos in a web service.
-
Resource server : A service that gives authorized client applications access to the resource owner’s protected resources. In OAuth 2.0, an authorization server grants authorization to a client application, based on the resource owner’s consent. For example, a resource server can be a web service that holds a user’s photos.
-
Client : An application that requests access to the resource owner’s protected resources, on behalf of the resource owner. For example, a client can be a photo printing service requesting access to a resource owner’s photos stored on a web service, after the resource owner gives the client consent to download the photos.
-
Authorization server : A service responsible for authenticating resource owners, and obtaining their consent to allow client applications to access their resources. For example, AM can act as the OAuth 2.0 authorization server to authenticate resource owners and obtain their consent. Other services, such as Google and Facebook can provide OAuth 2.0 authorization services.
The following image illustrates the steps for a client application to access a user’s protected resources, with AM as the authorization server and IG as the resource server:
-
The application obtains an authorization grant, representing the resource owner’s consent. For information about the different OAuth 2.0 grant mechanisms supported by AM, see OAuth 2.0 Grant Flows in AM’s OAuth 2.0 Guide.
-
The application authenticates to the authorization server and requests an access_token. The authorization server returns an access_token to the application.
An OAuth 2.0 access_token is an opaque string issued by the authorization server. When the client interacts with the resource server, the client presents the access_token in the
Authorization
header. For example:Authorization: Bearer 7af41ddd-47a4-40dc-b530-a9aa9f7ceda9
Access_tokens are the credentials to access protected resources. The advantage of access_tokens over passwords or other credentials is that access_tokens can be granted and revoked without exposing the user’s credentials.
The access_token represents the authorization to access protected resources. Because an access_token is a bearer token, anyone who has the access_token can use it to get the resources. Access_tokens must therefore be protected, so that requests involving them go over HTTPS.
In OAuth 2.0, the token scopes are strings that identify the scope of access authorized to the client, but can also be used for other purposes.
-
The application supplies the access_token to the resource server, which then resolves and validates the access_token by using an access_token resolver, as described in Access Token Resolvers.
If the access_token is valid, the resource server permits the client access to the requested resource.
Validate Access_Tokens Through the Introspection Endpoint
This section sets up IG as an OAuth 2.0 resource server, using the introspection endpoint.
For more information about configuring AM as an OAuth 2.0 authorization service, see AM’s OAuth 2.0 Guide.
Before you start, prepare AM, IG, and the sample application as described in Example Installation for This Guide.
-
Set up AM:
-
Select Applications > Agents > Identity Gateway, add an agent with the following values:
-
Agent ID :
ig_agent
-
Password :
password
-
Token Introspection :
Realm Only
-
-
Create an OAuth 2.0 Authorization Server:
-
Select Services > Add a Service > OAuth2 Provider.
-
Add a service with the default values.
-
-
Create an OAuth 2.0 Client to request OAuth 2.0 access_tokens:
-
Select Applications > OAuth 2.0 > Clients, and add a client with the following values:
-
Client ID :
client-application
-
Client secret :
password
-
Scope(s) :
mail
,employeenumber
-
-
(From AM 6.5) On the Advanced tab, select the following value:
-
Grant Types :
Resource Owner Password Credentials
-
-
-
-
Set up IG
-
Set an environment variable for the IG agent password, and then restart IG:
$ export AGENT_SECRET_ID='cGFzc3dvcmQ='
The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.
-
Add the following route to IG:
$HOME/.openig/config/routes/rs-introspect.json
appdata\OpenIG\config\routes\rs-introspect.json
{ "name": "rs-introspect", "baseURI": "http://app.example.com:8081", "condition": "${find(request.uri.path, '^/rs-introspect$')}", "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "AmService-1", "type": "AmService", "config": { "agent": { "username": "ig_agent", "passwordSecretId": "agent.secret.id" }, "secretsProvider": "SystemAndEnvSecretStore-1", "url": "http://openam.example.com:8088/openam/", "version": "7.1" } } ], "handler": { "type": "Chain", "config": { "filters": [ { "name": "OAuth2ResourceServerFilter-1", "type": "OAuth2ResourceServerFilter", "config": { "scopes": [ "mail", "employeenumber" ], "requireHttps": false, "realm": "OpenIG", "accessTokenResolver": { "name": "TokenIntrospectionAccessTokenResolver-1", "type": "TokenIntrospectionAccessTokenResolver", "config": { "amService": "AmService-1", "providerHandler": { "type": "Chain", "config": { "filters": [ { "type": "HttpBasicAuthenticationClientFilter", "config": { "username": "ig_agent", "passwordSecretId": "agent.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1" } } ], "handler": "ForgeRockClientHandler" } } } } } } ], "handler": { "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/html" ] }, "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>" } } } } }
For information about how to set up the IG route in Studio, see Token Validation Using the Introspection Endpoint in Structured Editor.
Notice the following features of the route:
-
The route matches requests to
/rs-introspect
. -
The
OAuth2ResourceServerFilter
expects an OAuth 2.0 access_token in the header of the incoming authorization request, with the scopesmail
andemployeenumber
.The
accessTokenResolver
uses the AM server declared in the heap. The introspection endpoint to validate the access_token is extrapolated from the URL of the AM server.For convenience in this test,
requireHttps
is false. In production environments, set it to true. -
After the filter validates the access_token, it creates a new context from the authorization server response. The context is named
oauth2
, and can be reached atcontexts.oauth2
orcontexts['oauth2']
.The context contains information about the access_token, which can be reached at
contexts.oauth2.accessToken.info
. Filters and handlers further down the chain can access the token info through the context.If there is no access_token in the request, or token validation does not complete successfully, the filter returns an HTTP error status to the user-agent, and IG does not continue processing the request. This is done as specified in the RFC, OAuth 2.0 Bearer Token Usage.
-
The HttpBasicAuthenticationClientFilter adds the credentials to the outgoing token introspection request.
-
The StaticResponseHandler returns the content of the access_token from the context
${contexts.oauth2.accessToken.info}
.
-
-
-
Test the setup:
-
In a terminal window, use a
curl
command similar to the following to retrieve an access_token:$ mytoken=$(curl -s \ --user "client-application:password" \ --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail%20employeenumber" \ http://openam.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
-
Validate the access_token returned in the previous step:
$ curl -v http://openig.example.com:8080/rs-introspect --header "Authorization: Bearer ${mytoken}" { active = true, scope = employeenumber mail, realm=/, client_id = client - application, user_id = demo, token_type = Bearer, exp = 158...907, ... }
-
Validate Stateless Access_Tokens With the StatelessAccessTokenResolver
The StatelessAccessTokenResolver confirms that stateless access_tokens provided by AM are well-formed, have a valid issuer, have the expected access_token name, and have a valid signature.
After the StatelessAccessTokenResolver resolves an access_token, the OAuth2ResourceServerFilter checks that the token is within the expiry time, and that it provides the required scopes. For more information, see StatelessAccessTokenResolver. This feature is supported with OpenAM 13.5, and AM 5 and later versions.
The following sections provide examples of how to validate signed and encrypted access_tokens:
Validate Signed Access_Tokens With the StatelessAccessTokenResolver and JwkSetSecretStore
This section provides examples of how to validate signed access_tokens with the StatelessAccessTokenResolver, using a JwkSetSecretStore. For more information about JwkSetSecretStore, see JwkSetSecretStore.
-
Set up AM:
-
Configure an OAuth 2.0 Authorization Provider:
-
Select Services, and add an OAuth 2.0 Provider.
-
Accept all of the default values, and select Create. The service is added to the Services list.
-
On the Core tab, select the following option:
-
Use Client-Based Access & Refresh Tokens :
on
-
-
On the Advanced tab, select the following options:
-
Client Registration Scope Whitelist :
myscope
-
OAuth2 Token Signing Algorithm :
RS256
-
Encrypt Client-Based Tokens : Deselected
-
-
-
Create an OAuth2 Client to request OAuth 2.0 access_tokens:
-
Select Applications > OAuth 2.0 > Clients, and add a client with the following values:
-
Client ID :
client-application
-
Client secret :
password
-
Scope(s) :
myscope
-
-
(From AM 6.5) On the Advanced tab, select the following values:
-
Grant Types :
Resource Owner Password Credentials
-
Response Types :
code token
-
-
On the Signing and Encryption tab, include the following setting:
-
ID Token Signing Algorithm :
RS256
-
-
-
-
Set up IG:
-
Add the following route to IG:
$HOME/.openig/config/routes/rs-stateless-signed.json
appdata\OpenIG\config\routes\rs-stateless-signed.json
{ "name": "rs-stateless-signed", "condition": "${find(request.uri.path, '/rs-stateless-signed')}", "heap": [ { "name": "SecretsProvider-1", "type": "SecretsProvider", "config": { "stores": [ { "type": "JwkSetSecretStore", "config": { "jwkUrl": "http://openam.example.com:8088/openam/oauth2/connect/jwk_uri" } } ] } } ], "handler": { "type": "Chain", "capture": "all", "config": { "filters": [ { "name": "OAuth2ResourceServerFilter-1", "type": "OAuth2ResourceServerFilter", "config": { "scopes": ["myscope"], "requireHttps": false, "accessTokenResolver": { "type": "StatelessAccessTokenResolver", "config": { "secretsProvider": "SecretsProvider-1", "issuer": "http://openam.example.com:8088/openam/oauth2", "verificationSecretId": "any.value.in.regex.format" } } } } ], "handler": { "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/html" ] }, "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>" } } } } }
Notice the following features of the route:
-
The route matches requests to
/rs-stateless-signed
. -
A SecretsProvider in the heap declares a JwkSetSecretStore to manage secrets for signed access_tokens.
-
The JwkSetSecretStore specifies the URL to a JWK set on AM, that contains the signing keys.
-
The OAuth2ResourceServerFilter expects an OAuth 2.0 access_token in the header of the incoming authorization request, with the scope
myscope
. -
The
StatelessAccessTokenResolver
uses the SecretsProvider to verify the signature of the provided access_token. -
After the OAuth2ResourceServerFilter validates the access_token, it creates the
OAuth2Context
context. For more information, see OAuth2Context. -
If there is no access_token in a request, or token validation does not complete successfully, the filter returns an HTTP error status to the user-agent, and IG does not continue processing the request. This is done as specified in the RFC OAuth 2.0 Bearer Token Usage.
-
The StaticResponseHandler returns the content of the access_token from the context.
-
-
-
Test the setup for a signed access_token:
-
Get an access_token for the demo user, using the scope
myscope
:$ mytoken=$(curl -s \ --user "client-application:password" \ --data "grant_type=password&username=demo&password=Ch4ng31t&scope=myscope" \ http://openam.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
-
Display the token:
$ echo ${mytoken}
Note that the token is structured as a signed token.
-
Access the route by providing the token returned in the previous step:
$ curl -v http://openig.example.com:8080/rs-stateless-signed --header "Authorization: Bearer ${mytoken}" ... Decoded access_token: { sub=demo, cts=OAUTH2_STATELESS_GRANT, ...
-
Validate Signed Access_Tokens With the StatelessAccessTokenResolver and KeyStoreSecretStore
This section provides examples of how to validate signed access_tokens with the StatelessAccessTokenResolver, using a KeyStoreSecretStore. For more information about KeyStoreSecretStore, see KeyStoreSecretStore.
-
Locate the following directories for keys, keystores, and certificates, and in a terminal create variables for them:
-
Directory where the keystore is created:
keystore_directory
-
AM keystore directory:
am_keystore_directory
-
IG keystore directory:
ig_keystore_directory
-
-
Set up the keystore for signing keys:
-
Generate a private key called
signature-key
, and a corresponding public certificate calledx509certificate.pem
:$ openssl req -x509 \ -newkey rsa:2048 \ -nodes \ -subj "/CN=openig.example.com/OU=example/O=com/L=fr/ST=fr/C=fr" \ -keyout $keystore_directory/signature-key.key \ -out $keystore_directory/x509certificate.pem \ -days 365 ... writing new private key to '$keystore_directory/signature-key.key'
-
Convert the private key and certificate files into a PKCS12 file, called
signature-key
, and store them in a keystore namedkeystore.p12
:$ openssl pkcs12 \ -export \ -in $keystore_directory/x509certificate.pem \ -inkey $keystore_directory/signature-key.key \ -out $keystore_directory/keystore.p12 \ -passout pass:password \ -name signature-key
-
List the keys in
keystore.p12
:$ keytool -list \ -v \ -keystore "$keystore_directory/keystore.p12" \ -storepass "password" \ -storetype PKCS12 ... Your keystore contains 1 entry Alias name: signature-key
-
-
Set up keys for AM:
-
Copy the signing key
keystore.p12
to AM:$ cp $keystore_directory/keystore.p12 $am_keystore_directory/AM_keystore.p12
-
List the keys in the AM keystore:
$ keytool -list \ -v \ -keystore "$am_keystore_directory/AM_keystore.p12" \ -storepass "password" \ -storetype PKCS12 ... Your keystore contains 1 entry Alias name: signature-key
-
Add a file called
keystore.pass
, containing the store passwordpassword
:$ cd $am_keystore_directory $ echo -n 'password' > keystore.pass
Make sure that the password file contains only the password, with no trailing spaces or carriage returns.
The filename corresponds to the secret ID of the store password and entry password for the KeyStoreSecretStore.
-
Restart AM.
-
-
Set up keys for IG:
-
Import the public certificate to the IG keystore, with the alias
verification-key
:$ keytool -import \ -trustcacerts \ -rfc \ -alias verification-key \ -file "$keystore_directory/x509certificate.pem" \ -keystore "$ig_keystore_directory/IG_keystore.p12" \ -storetype PKCS12 \ -storepass "password" ... Trust this certificate? [no]: yes Certificate was added to keystore
-
List the keys in the IG keystore:
$ keytool -list \ -v \ -keystore "$ig_keystore_directory/IG_keystore.p12" \ -storepass "password" \ -storetype PKCS12 ... Your keystore contains 1 entry Alias name: verification-key
-
In the IG configuration, set an environment variable for the keystore password:
$ export KEYSTORE_SECRET_ID='cGFzc3dvcmQ='
-
Restart IG.
-
-
Set up AM:
-
Create a KeyStoreSecretStore to manage the new AM keystore:
-
In AM, select Secret Stores, and then add a secret store with the following values:
-
Secret Store ID :
keystoresecretstore
-
Store Type :
Keystore
-
File :
am_keystore_directory/AM_keystore.p12
-
Keystore type :
PKCS12
-
Store password secret ID :
keystore.pass
-
Entry password secret ID :
keystore.pass
-
-
Select the Mappings tab, and add a mapping with the following values:
-
Secret ID :
am.services.oauth2.stateless.signing.RSA
-
Aliases :
signature-key
The mapping sets
signature-key
as the active alias to use for signature generation.
-
-
-
Create a FileSystemSecretStore to manage secrets for the KeyStoreSecretStore:
-
Select Secret Stores, and then create a secret store with the following configuration:
-
Secret Store ID :
filesystemsecretstore
-
Store Type :
File System Secret Volumes
-
Directory :
am_keystore_directory/secrets
-
File format :
Plain text
-
-
-
Configure an OAuth 2.0 Authorization Provider:
-
Select Services, and add an OAuth 2.0 Provider.
-
Accept all of the default values, and select Create. The service is added to the Services list.
-
On the Core tab, select the following option:
-
Use Client-Based Access & Refresh Tokens :
on
-
-
On the Advanced tab, select the following options:
-
Client Registration Scope Whitelist :
myscope
-
OAuth2 Token Signing Algorithm :
RS256
-
Encrypt Client-Based Tokens : Deselected
-
-
-
Create an OAuth2 Client to request OAuth 2.0 access_tokens:
-
Select Applications > OAuth 2.0 > Clients, and add a client with the following values:
-
Client ID :
client-application
-
Client secret :
password
-
Scope(s) :
myscope
-
-
(From AM 6.5) On the Advanced tab, select the following values:
-
Grant Types :
Resource Owner Password Credentials
-
Response Types :
code token
-
-
On the Signing and Encryption tab, include the following setting:
-
ID Token Signing Algorithm :
RS256
-
-
-
-
Set up IG:
-
Add the following route to IG, and replace ig_keystore_directory:
$HOME/.openig/config/routes/rs-stateless-signed-ksss.json
appdata\OpenIG\config\routes\rs-stateless-signed-ksss.json
{ "name": "rs-stateless-signed-ksss", "condition" : "${matches(request.uri.path, '/rs-stateless-signed-ksss')}", "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "KeyStoreSecretStore-1", "type": "KeyStoreSecretStore", "config": { "file": "<ig_keystore_directory>/IG_keystore.p12", "storeType": "PKCS12", "storePassword": "keystore.secret.id", "keyEntryPassword": "keystore.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1", "mappings": [ { "secretId": "stateless.access.token.verification.key", "aliases": [ "verification-key" ] } ] } } ], "handler" : { "type" : "Chain", "capture" : "all", "config" : { "filters" : [ { "name" : "OAuth2ResourceServerFilter-1", "type" : "OAuth2ResourceServerFilter", "config" : { "scopes" : [ "myscope" ], "requireHttps" : false, "accessTokenResolver": { "type": "StatelessAccessTokenResolver", "config": { "secretsProvider": "KeyStoreSecretStore-1", "issuer": "http://openam.example.com:8088/openam/oauth2", "verificationSecretId": "stateless.access.token.verification.key" } } } } ], "handler": { "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/html" ] }, "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>" } } } } }
Notice the following features of the route:
-
The route matches requests to
/rs-stateless-signed-ksss
. -
The keystore password is provided by the SystemAndEnvSecretStore in the heap.
-
The OAuth2ResourceServerFilter expects an OAuth 2.0 access_token in the header of the incoming authorization request, with the scope
myscope
. -
The
accessTokenResolver
uses aStatelessAccessTokenResolver
to resolve and verify the authenticity of the access_token. The secret is provided by the KeyStoreSecretStore in the heap. -
After the OAuth2ResourceServerFilter validates the access_token, it creates the
OAuth2Context
context. For more information, see OAuth2Context. -
If there is no access_token in a request, or if the token validation does not complete successfully, the filter returns an HTTP error status to the user-agent, and IG stops processing the request, as specified in the RFC, OAuth 2.0 Bearer Token Usage.
-
The StaticResponseHandler returns the content of the access_token from the context.
-
-
-
Test the setup for a signed access_token:
-
Get an access_token for the demo user, using the scope
myscope
:$ mytoken=$(curl -s \ --user "client-application:password" \ --data "grant_type=password&username=demo&password=Ch4ng31t&scope=myscope" \ http://openam.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
-
Display the token:
$ echo ${mytoken}
-
Access the route by providing the token returned in the previous step:
$ curl -v http://openig.example.com:8080/rs-stateless-signed-ksss --header "Authorization: Bearer ${mytoken}" ... Decoded access_token: { sub=demo, cts=OAUTH2_STATELESS_GRANT, ...
-
Validating Encrypted Access_Tokens With the StatelessAccessTokenResolver and KeyStoreSecretStore
-
Locate the following directories for keys, keystores, and certificates, and in a terminal create variables for them:
-
Directory where the keystore is created:
keystore_directory
-
AM keystore directory:
am_keystore_directory
-
IG keystore directory:
ig_keystore_directory
-
-
Set up keys for AM:
-
Generate the encryption key:
$ keytool -genseckey \ -alias encryption-key \ -dname "CN=openig.example.com, OU=example, O=com, L=fr, ST=fr, C=fr" \ -keystore "$am_keystore_directory/AM_keystore.p12" \ -storetype PKCS12 \ -storepass "password" \ -keyalg AES \ -keysize 256
-
List the keys in the AM keystore:
$ keytool -list \ -v \ -keystore "$am_keystore_directory/AM_keystore.p12" \ -storepass "password" \ -storetype PKCS12 ... Your keystore contains 1 entry Alias name: encryption-key
-
Add a file called
keystore.pass
, with the contentpassword
:$ cd $am_keystore_directory $ echo -n 'password' > keystore.pass
Make sure that the password file contains only the password, with no trailing spaces or carriage returns. The filename corresponds to the secret ID of the store password and entry password for the KeyStoreSecretStore.
-
Restart AM.
-
-
Set up keys for IG:
-
Import
encryption-key
into the IG keystore, with the aliasdecryption-key
:$ keytool -importkeystore \ -srcalias encryption-key \ -srckeystore "$am_keystore_directory/AM_keystore.p12" \ -srcstoretype PKCS12 \ -srcstorepass "password" \ -destkeystore "$ig_keystore_directory/IG_keystore.p12" \ -deststoretype PKCS12 \ -destalias decryption-key \ -deststorepass "password" \ -destkeypass "password"
-
List the keys in the IG keystore:
$ keytool -list \ -v \ -keystore "$ig_keystore_directory/IG_keystore.p12" \ -storepass "password" \ -storetype PKCS12 ... Your keystore contains 1 entry Alias name: decryption-key
-
In the IG configuration, set an environment variable for the keystore password:
$ export KEYSTORE_SECRET_ID='cGFzc3dvcmQ='
-
Restart IG.
-
-
Set up AM:
-
Set up AM as described in Validate Signed Access_Tokens With the StatelessAccessTokenResolver and KeyStoreSecretStore.
-
Add a mapping for the encryption keystore:
-
Select Secret Stores >
keystoresecretstore
. -
Select the Mappings tab, and add a mapping with the following values:
-
Secret ID :
am.services.oauth2.stateless.token.encryption
-
Alias :
encryption-key
-
-
-
Enable token encryption on the OAuth 2.0 Authorization Provider:
-
Select Services > OAuth2 Provider.
-
On the Advanced tab, select Encrypt Client-Based Tokens.
-
-
-
Set up IG:
-
Add the following route to IG, and replace ig_keystore_directory:
$HOME/.openig/config/routes/rs-stateless-encrypted.json
appdata\OpenIG\config\routes\rs-stateless-encrypted.json
{ "name": "rs-stateless-encrypted", "condition": "${find(request.uri.path, '/rs-stateless-encrypted')}", "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "KeyStoreSecretStore-1", "type": "KeyStoreSecretStore", "config": { "file": "<ig_keystore_directory>/IG_keystore.p12", "storeType": "PKCS12", "storePassword": "keystore.secret.id", "keyEntryPassword": "keystore.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1", "mappings": [ { "secretId": "stateless.access.token.decryption.key", "aliases": [ "decryption-key" ] } ] } } ], "handler": { "type": "Chain", "capture": "all", "config": { "filters": [ { "name": "OAuth2ResourceServerFilter-1", "type": "OAuth2ResourceServerFilter", "config": { "scopes": [ "myscope" ], "requireHttps": false, "accessTokenResolver": { "type": "StatelessAccessTokenResolver", "config": { "secretsProvider": "KeyStoreSecretStore-1", "issuer": "http://openam.example.com:8088/openam/oauth2", "decryptionSecretId": "stateless.access.token.decryption.key" } } } } ], "handler": { "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/html" ] }, "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>" } } } } }
Notice the following features of the route compared to
rs-stateless-signed.json
, used in: Validate Signed Access_Tokens With the StatelessAccessTokenResolver and KeyStoreSecretStore:-
The route matches requests to
/rs-stateless-encrypted
. -
The OAuth2ResourceServerFilter and KeyStoreSecretStore refer to the configuration for a decryption key instead of a verification key.
-
-
-
Get an access_token for the demo user, using the scope
myscope
:$ mytoken=$(curl -s \ --user "client-application:password" \ --data "grant_type=password&username=demo&password=Ch4ng31t&scope=myscope" \ http://openam.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
-
Display the token:
$ echo ${mytoken}
Note that the token is structured as an encrypted token.
-
Access the route by providing the token returned in the previous step:
$ curl -v http://openig.example.com:8080/rs-stateless-encrypted --header "Authorization: Bearer ${mytoken}" ... Decoded access_token: { sub=demo, cts=OAUTH2_STATELESS_GRANT, ...
Validate Certificate-Bound Access Tokens
Clients can authenticate to AM through mutual TLS (mTLS) and X.509 certificates. Certificates must be self-signed or use public key infrastructure (PKI), as described in version 12 of the draft OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens.
When a client requests an access_token from AM through mTLS,
AM can use a confirmation key to bind the access_token to the
presented client certificate. The confirmation key is the certificate
thumbprint, computed as
base64url-encode(sha256(der(certificate)))
. The access_token is then
certificate-bound. For more information, see
Authenticating Clients Using Mutual TLS in AM’s OAuth 2.0 Guide.
When the client connects to IG by using that certificate, IG can verify that the confirmation key corresponds to the presented certificate. This proof-of-possession interaction ensures that only the client in possession of the key corresponding to the certificate can use the access_token to access protected resources.
mTLS Using Standard TLS Client Certificate Authentication
IG can validate the thumbprint of certificate-bound access_tokens by reading the client certificate from the TLS connection. When the web container that is running IG performs a successful TLS connection handshake, the connected client is trusted.
For this example, the client must be connected directly to IG through a TLS connection, for which IG is the TLS termination point. If TLS is terminated at a reverse proxy or load balancer before IG, use the example in mTLS Using Trusted Headers.
Perform the procedures in this section to set up and test mTLS using standard TLS client certificate authentication:
-
Locate the following keystore directories, and in a terminal create variables for them:
-
oauth2_client_keystore_directory
-
am_keystore_directory
-
ig_keystore_directory
-
-
Create self-signed RSA key pairs for AM, IG, and the client:
$ keytool -genkeypair \ -alias openam-server \ -keyalg RSA \ -keysize 2048 \ -keystore $am_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -keypass changeit \ -validity 360 \ -dname CN=openam.example.com,O=Example,C=FR
$ keytool -genkeypair \ -alias openig-server \ -keyalg RSA \ -keysize 2048 \ -keystore $ig_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -keypass changeit \ -validity 360 \ -dname CN=openig.example.com,O=Example,C=FR
$ keytool -genkeypair \ -alias oauth2-client \ -keyalg RSA \ -keysize 2048 \ -keystore $oauth2_client_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -keypass changeit \ -validity 360 \ -dname CN=test
-
Export the certificates to .pem so that the
curl
client can verify the identity of the AM and IG servers:$ keytool -export \ -rfc \ -alias openam-server \ -keystore $am_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -file $am_keystore_directory/openam-server.cert.pem Certificate stored in file .../openam-server.cert.pem
$ keytool -export \ -rfc \ -alias openig-server \ -keystore $ig_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -file $ig_keystore_directory/openig-server.cert.pem Certificate stored in file openig-server.cert.pem
-
Extract the certificate and client private key to .pem so that the
curl
command can identity itself as the client for the HTTPS connection:$ keytool -export \ -rfc \ -alias oauth2-client \ -keystore $oauth2_client_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -file $oauth2_client_keystore_directory/client.cert.pem Certificate stored in file .../client.cert.pem
$ openssl pkcs12 \ -in $oauth2_client_keystore_directory/keystore.p12 \ -nocerts \ -nodes \ -passin pass:changeit \ -out $oauth2_client_keystore_directory/client.key.pem ...verified OK
You can now delete the client keystore.
-
Create the CACerts truststore so that AM can validate the client identity:
$ keytool -import \ -noprompt \ -trustcacerts \ -file $oauth2_client_keystore_directory/client.cert.pem \ -keystore $oauth2_client_keystore_directory/cacerts.p12 \ -storepass changeit \ -storetype PKCS12 \ -alias client-cert Certificate was added to keystore
This procedure sets up AM for HTTPS in Tomcat. For more information, see Configuring AM’s Container for HTTPS in AM’s Installation Guide.
-
Add the following connector configuration to AM’s Tomcat
server.xml
, replacing the values for the keystore directories with your paths:<Connector port="8445" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true"> <SSLHostConfig protocols="+TLSv1.2,-TLSv1.1,-TLSv1,-SSLv2Hello,-SSLv3" certificateVerification="optionalNoCA" truststoreFile="oauth2_client_keystore_directory/cacerts.p12" truststorePassword="changeit" truststoreType="PKCS12"> <Certificate certificateKeystoreFile="am_keystore_directory/keystore.p12" certificateKeystorePassword="changeit" certificateKeystoreType="PKCS12"/> </SSLHostConfig> </Connector>
The
optionalNoCA
property allows the presentation of client certificates to be optional. Tomcat does not check them against the list of trusted CAs. -
In AM, export an environment variable for the base64-encoded value of the password (
changeit
) for thecacerts.p12
truststore:$ export PASSWORDSECRETID='Y2hhbmdlaXQ='
-
Restart AM, and make sure that you can access it on the secure port https://openam.example.com:8445/openam.
This procedure sets up IG for HTTPS in Tomcat. For other container types, see Configure IG for HTTPS (Server-Side) in Jetty and Configure IG for HTTPS (Server-Side) in JBoss EAP.
If IG is installed in standalone mode, follow Set Up IG for HTTPS (Server-Side) in Standalone Mode instead.
-
Add the following connector configuration to IG’s Tomcat
server.xml
, replacing the values for the keystore directories with your paths:<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true"> <SSLHostConfig protocols="+TLSv1.2,-TLSv1.1,-TLSv1,-SSLv2Hello,-SSLv3" certificateVerification="optionalNoCA" truststoreFile="oauth2_client_keystore_directory/cacerts.p12" truststorePassword="changeit" truststoreType="PKCS12"> <Certificate certificateKeystoreFile="ig_keystore_directory/keystore.p12" certificateKeystorePassword="changeit" certificateKeystoreType="PKCS12" /> </SSLHostConfig> </Connector>
The
optionalNoCA
property allows the presentation of client certificates to be optional. Tomcat does not check them against the list of trusted CAs. -
Restart IG, and make sure that you can access the welcome page on the secure port https://openig.example.com:8443.
This procedure sets up IG for HTTPS in standalone mode. Before you start, install IG in standalone mode, as described in Download and Start IG in Standalone Mode.
When IG is installed in web container mode, follow Set Up IG for HTTPS (Server-Side) in Tomcat instead.
-
In ig_keystore_directory, add a file called
keystore.pass
containing the keystore password:$ cd $ig_keystore_directory $ echo -n 'changeit' > keystore.pass
Make sure that the password file contains only the password, with no trailing spaces or carriage returns. -
Add the following route to IG, replacing instances of ig_keystore_directory and oauth2_client_keystore_directory with your path:
{ "mode": "DEVELOPMENT", "connectors": [ { "port": 8080 }, { "port": 8443, "tls": { "type": "ServerTlsOptions", "config": { "alpn": { "enabled": true }, "clientAuth": "REQUEST", "keyManager": { "type": "SecretsKeyManager", "config": { "signingSecretId": "key.manager.secret.id", "secretsProvider": { "type": "KeyStoreSecretStore", "config": { "file": "<ig_keystore_directory>/keystore.p12", "storePassword": "keystore.pass", "secretsProvider": "SecretsPasswords", "mappings": [ { "secretId": "key.manager.secret.id", "aliases": [ "openig-server" ] } ] } } } }, "trustManager": { "type": "SecretsTrustManager", "config": { "verificationSecretId": "trust.manager.secret.id", "secretsProvider": { "type": "KeyStoreSecretStore", "config": { "file": "<oauth2_client_keystore_directory>/cacerts.p12", "storePassword": "keystore.pass", "secretsProvider": "SecretsPasswords", "mappings": [ { "secretId": "trust.manager.secret.id", "aliases": [ "client-cert" ] } ] } } } } } } } ], "heap": [ { "name": "SecretsPasswords", "type": "FileSystemSecretStore", "config": { "directory": "<ig_keystore_directory>", "format": "PLAIN" } } ] }
Notice the following features of the route:
-
IG starts on port
8080
, and on8443
over TLS. -
IG’s private keys for TLS are managed by the SecretsKeyManager, which references the KeyStoreSecretStore that holds the keys.
-
The password of the KeyStoreSecretStore is provided by the FileSystemSecretStore.
-
The KeyStoreSecretStore maps the keystore alias to the secret ID for retrieving the private signing keys.
-
-
Start IG:
$ /path/to/identity-gateway/bin/start.sh ... ... started in 1234ms on ports : [8080 8443]
Before you start, install and configure AM on http://openam.example.com:8088/openam.
-
Select Applications > Agents > Identity Gateway, add an agent with the following values:
-
Agent ID :
ig_agent
-
Password :
password
-
Token Introspection :
Realm Only
-
-
Configure an OAuth 2.0 Authorization Server:
-
Select Services > Add a Service > OAuth2 Provider, and add a service with the default values.
-
On the Advanced tab, select the following value:
-
Support TLS Certificate-Bound Access Tokens : enabled
-
-
-
Configure an OAuth 2.0 client to request access_tokens:
-
Select Applications > OAuth 2.0 > Clients, and add a client with the following values:
-
Client ID :
client-application
-
Client secret :
password
-
Scope(s) :
test
-
-
On the Advanced tab, select the following values:
-
Grant Types :
Client Credentials
The
password
is the only grant type used by the client in the example. -
Token Endpoint Authentication Method :
tls_client_auth
-
-
On the signing and Encryption tab, select the following values:
-
mTLS Subject DN :
CN=test
When this option is set, AM requires the subject DN in the client certificate to have the same value. This ensures that the certificate is from the client, and not just any valid certificate trusted by the trust manager.
-
Use Certificate-Bound Access Tokens : Enabled
-
-
-
Set up AM secret stores to trust the client certificate:
-
Select Secret Stores, and add a store with the following values:
-
Secret Store ID :
trusted-ca-certs
-
Store Type :
Keystore
-
File :
$oauth2_client_keystore_directory/cacerts.p12
-
Keystore type :
PKCS12
-
Store password secret ID :
passwordSecretId
-
-
Select Mappings and add the following mapping:
-
Secret ID :
am.services.oauth2.tls.client.cert.authentication
-
Aliases :
client-cert
-
When the token endpoint authentication method is
tls_client_auth
, this secret is used to validate the client certificate. Add an alias in this list for each client that usestls_client_auth
. For certificates signed by a CA, add the CA certificate to the list. -
-
Set an environment variable for the IG agent password, and then restart IG:
$ export AGENT_SECRET_ID='cGFzc3dvcmQ='
The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.
-
Add the following route to IG:
$HOME/.openig/config/routes/mtls-certificate.json
appdata\OpenIG\config\routes\mtls-certificate.json
{ "name": "mtls-certificate", "condition": "${find(request.uri.path, '/mtls-certificate')}", "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "AmService-1", "type": "AmService", "config": { "agent": { "username": "ig_agent", "passwordSecretId": "agent.secret.id" }, "secretsProvider": "SystemAndEnvSecretStore-1", "url": "http://openam.example.com:8088/openam/", "version": "7.1" } } ], "handler": { "type": "Chain", "capture": "all", "config": { "filters": [ { "name": "OAuth2ResourceServerFilter-1", "type": "OAuth2ResourceServerFilter", "config": { "scopes": [ "test" ], "requireHttps": false, "accessTokenResolver": { "type": "ConfirmationKeyVerifierAccessTokenResolver", "config": { "delegate": { "name": "token-resolver-1", "type": "TokenIntrospectionAccessTokenResolver", "config": { "amService": "AmService-1", "providerHandler": { "type": "Chain", "config": { "filters": [ { "type": "HttpBasicAuthenticationClientFilter", "config": { "username": "ig_agent", "passwordSecretId": "agent.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1" } } ], "handler": "ForgeRockClientHandler" } } } } } } } } ], "handler": { "name": "StaticResponseHandler-1", "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/plain" ] }, "entity": "mTLS\n Valid token: ${contexts.oauth2.accessToken.token}\n Confirmation keys: ${contexts.oauth2}" } } } } }
Notice the following features of the route:
-
The route matches requests to
/mtls-certificate
. -
The OAuth2ResourceServerFilter uses the ConfirmationKeyVerifierAccessTokenResolver to validate the certificate thumbprint against the thumbprint from the resolved access_token, provided by AM.
The ConfirmationKeyVerifierAccessTokenResolver then delegates token resolution to the TokenIntrospectionAccessTokenResolver.
-
The
providerHandler
adds an authorization header to the request, containing the username and password of the OAuth 2.0 client with the scope to examine (introspect) access_tokens. -
The OAuth2ResourceServerFilter checks that the resolved token has the required scopes, and injects the token info into the context.
-
The StaticResponseHandler returns the content of the access_token from the context.
-
-
Get an access_token from AM, over TLS:
$ mytoken=$(curl --request POST \ --cacert $am_keystore_directory/openam-server.cert.pem \ --cert $oauth2_client_keystore_directory/client.cert.pem \ --key $oauth2_client_keystore_directory/client.key.pem \ --header 'cache-control: no-cache' \ --header 'content-type: application/x-www-form-urlencoded' \ --data 'client_id=client-application&grant_type=client_credentials&scope=test' \ https://openam.example.com:8445/openam/oauth2/access_token | jq -r .access_token)
-
Introspect the access_token on AM:
$ curl --request POST \ -u ig_agent:password \ --header 'content-type: application/x-www-form-urlencoded' \ --data token=${mytoken} \ http://openam.example.com:8088/openam/oauth2/realms/root/introspect | jq { "active": true, "scope": "test", "realm": "/", "client_id": "client-application", "user_id": "client-application", "token_type": "Bearer", "exp": 155...833, "sub": "(age!client-application)", "subname": "client-application", "iss": "http://openam.example.com:8088/openam/oauth2", "cnf": { "x5t#S256": "T4u...R9Q" }, "authGrantId": "dfE...2vk", "auditTrackingId": "e366...524" }
The
cnf
property indicates the value of the confirmation code, as follows:-
x5
: X509 certificate -
t
: thumbprint -
#
: separator -
S256
: algorithm used to hash the raw certificate bytes
-
-
Access the IG route to validate the token’s confirmation thumbprint with the ConfirmationKeyVerifierAccessTokenResolver:
$ curl --request POST \ --cacert $ig_keystore_directory/openig-server.cert.pem \ --cert $oauth2_client_keystore_directory/client.cert.pem \ --key $oauth2_client_keystore_directory/client.key.pem \ --header "authorization: Bearer ${mytoken}" \ https://openig.example.com:8443/mtls-certificate mTLS Valid token: 2Bp...s_k Confirmation keys: { ... }
The validated token and confirmation keys are displayed.
mTLS Using Trusted Headers
IG can validate the thumbprint of certificate-bound access_tokens by reading the client certificate from a configured, trusted HTTP header.
Use this method when TLS is terminated at a reverse proxy or load balancer before IG. IG cannot authenticate the client through the TLS connection’s client certificate because:
-
If the connection is over TLS, the connection presents the certificate of the TLS termination point before IG.
-
If the connection is not over TLS, the connection presents no client certificate.
If the client is connected directly to IG through a TLS connection, for which IG is the TLS termination point, use the example in mTLS Using Standard TLS Client Certificate Authentication.
Configure the proxy or load balancer to:
-
Forward the encoded certificate to IG in the trusted header.
Encode the certificate in an HTTP-header compatible format that can convey a full certificate, so that IG can rebuild the certificate.
-
Strip the trusted header from incoming requests, and change the default header name to something an attacker can’t guess.
Because there is a trust relationship between IG and the TLS termination point, IG doesn’t authenticate the contents of the trusted header. IG accepts any value in a header from a trusted TLS termination point.
Use this example when the IG instance is running behind a load balancer or other ingress point. If the IG instance is running behind the TLS termination point, consider the example in mTLS Using Standard TLS Client Certificate Authentication.
The following image illustrates the connections and certificates required by the example:
-
Set up the keystores, truststores, AM, and IG as described in mTLS Using Standard TLS Client Certificate Authentication.
-
Base64-encode the value of
$oauth2_client_keystore_directory/client.cert.pem
. The value is used in the final POST. -
Add the following route to IG:
$HOME/.openig/config/routes/mtls-header.json
appdata\OpenIG\config\routes\mtls-header.json
{ "name": "mtls-header", "condition": "${find(request.uri.path, '/mtls-header')}", "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "AmService-1", "type": "AmService", "config": { "agent": { "username": "ig_agent", "passwordSecretId": "agent.secret.id" }, "secretsProvider": "SystemAndEnvSecretStore-1", "url": "http://openam.example.com:8088/openam/", "version": "7.1" } } ], "handler": { "type": "Chain", "capture": "all", "config": { "filters": [ { "name": "CertificateThumbprintFilter-1", "type": "CertificateThumbprintFilter", "config": { "certificate": "${pemCertificate(decodeBase64(request.headers['ssl_client_cert'][0]))}", "failureHandler": { "type": "ScriptableHandler", "config": { "type": "application/x-groovy", "source": [ "def response = new Response(Status.TEAPOT);", "response.entity = 'Failure in CertificateThumbprintFilter'", "return response" ] } } } }, { "name": "OAuth2ResourceServerFilter-1", "type": "OAuth2ResourceServerFilter", "config": { "scopes": [ "test" ], "requireHttps": false, "accessTokenResolver": { "type": "ConfirmationKeyVerifierAccessTokenResolver", "config": { "delegate": { "name": "token-resolver-1", "type": "TokenIntrospectionAccessTokenResolver", "config": { "amService": "AmService-1", "providerHandler": { "type": "Chain", "config": { "filters": [ { "type": "HttpBasicAuthenticationClientFilter", "config": { "username": "ig_agent", "passwordSecretId": "agent.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1" } } ], "handler": "ForgeRockClientHandler" } } } } } } } } ], "handler": { "name": "StaticResponseHandler-1", "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/plain" ] }, "entity": "mTLS\n Valid token: ${contexts.oauth2.accessToken.token}\n Confirmation keys: ${contexts.oauth2}" } } } } }
Notice the following features of the route compared to
mtls-certificate.json
:-
The route matches requests to
/mtls-header
. -
The CertificateThumbprintFilter extracts a Java certificate from the trusted header, computes the SHA-256 thumbprint of that certificate, and makes the thumbprint available for the ConfirmationKeyVerifierAccessTokenResolver.
-
-
Test the setup:
-
Get an access_token from AM, over TLS:
$ mytoken=$(curl --request POST \ --cacert $am_keystore_directory/openam-server.cert.pem \ --cert $oauth2_client_keystore_directory/client.cert.pem \ --key $oauth2_client_keystore_directory/client.key.pem \ --header 'cache-control: no-cache' \ --header 'content-type: application/x-www-form-urlencoded' \ --data 'client_id=client-application&grant_type=client_credentials&scope=test' \ https://openam.example.com:8445/openam/oauth2/access_token | jq -r .access_token)
-
Introspect the access_token on AM:
$ curl --request POST \ -u ig_agent:password \ --header 'content-type: application/x-www-form-urlencoded' \ --data token=${mytoken} \ http://openam.example.com:8088/openam/oauth2/realms/root/introspect | jq { "active": true, "scope": "test", "realm": "/", "client_id": "client-application", "user_id": "client-application", "token_type": "Bearer", "exp": 157...994, "sub": "(age!client-application)", "subname": "client-application", "iss": "http://openam.example.com:8088/openam/oauth2", "cnf": { "x5t#S256": "1QG...Wgc" }, "authGrantId": "lto...8vw", "auditTrackingId": "119...480" }
The
cnf
property indicates the value of the confirmation code, as follows:-
x5
: X509 certificate -
t
: thumbprint -
#
: separator -
S256
: algorithm used to hash the raw certificate bytes
-
-
Access the IG route to validate the confirmation key, using the base64-encoded value of
$oauth2_client_keystore_directory/client.cert.pem
:$ curl --request POST \ --header "authorization:Bearer $mytoken" \ --header 'ssl_client_cert:base64-encoded-cert' http://openig.example.com:8080/mtls-header Valid token: zw5...Sj1 Confirmation keys: { ... }
The validated token and confirmation keys are displayed.
-
Use the OAuth 2.0 Context to Log in to the Sample Application
The introspection returns scopes in the context. This section contains an example route that retrieves the scopes, assigns them as the IG session username and password, and uses them to log the user directly in to the sample application.
For information about the context, see OAuth2Context.
-
Set up AM:
-
Set up AM as described in Validate Access_Tokens Through the Introspection Endpoint.
-
Select Identities, and change the email address of the demo user to
demo
. -
Select (scripts) > OAuth2 Access Token Modification Script, and replace the default script as follows:
import org.forgerock.http.protocol.Request import org.forgerock.http.protocol.Response import com.iplanet.sso.SSOException import groovy.json.JsonSlurper def attributes = identity.getAttributes(["mail"].toSet()) accessToken.setField("mail", attributes["mail"][0]) accessToken.setField("password", "{amDemoPw}")
The AM script adds user profile information to the access_token, and adds a
password
field with the valueCh4ng31t
.Do not use this example in production! If the token is stateless and unencrypted, the password value is easily accessible when you have the token.
-
-
Set up IG:
-
Set an environment variable for the IG agent password, and then restart IG:
$ export AGENT_SECRET_ID='cGFzc3dvcmQ='
The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.
-
Add the following route to IG:
$HOME/.openig/config/routes/rs-pwreplay.json
appdata\OpenIG\config\routes\rs-pwreplay.json
{ "name" : "rs-pwreplay", "baseURI" : "http://app.example.com:8081", "condition" : "${matches(request.uri.path, '^/rs-pwreplay')}", "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "AmService-1", "type": "AmService", "config": { "agent": { "username": "ig_agent", "passwordSecretId": "agent.secret.id" }, "secretsProvider": "SystemAndEnvSecretStore-1", "url": "http://openam.example.com:8088/openam/", "version": "7.1" } } ], "handler" : { "type" : "Chain", "config" : { "filters" : [ { "name" : "OAuth2ResourceServerFilter-1", "type" : "OAuth2ResourceServerFilter", "config" : { "scopes" : [ "mail", "employeenumber" ], "requireHttps" : false, "realm" : "OpenIG", "accessTokenResolver": { "name": "TokenIntrospectionAccessTokenResolver-1", "type": "TokenIntrospectionAccessTokenResolver", "config": { "amService": "AmService-1", "providerHandler": { "type": "Chain", "config": { "filters": [ { "type": "HttpBasicAuthenticationClientFilter", "config": { "username": "ig_agent", "passwordSecretId": "agent.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1" } } ], "handler": "ForgeRockClientHandler" } } } } } }, { "type": "AssignmentFilter", "config": { "onRequest": [{ "target": "${session.username}", "value": "${contexts.oauth2.accessToken.info.mail}" }, { "target": "${session.password}", "value": "${contexts.oauth2.accessToken.info.password}" } ] } }, { "type": "StaticRequestFilter", "config": { "method": "POST", "uri": "http://app.example.com:8081/login", "form": { "username": [ "${session.username}" ], "password": [ "${session.password}" ] } } } ], "handler": "ReverseProxyHandler" } } }
Notice the following features of the route compared to
rs-introspect.json
:-
The route matches requests to
/rs-pwreplay
. -
The AssignmentFilter accesses the context, and injects the username and password into the SessionContext,
${session}
. -
The StaticRequestFilter retrieves the username and password from
session
, and replaces the original HTTP GET request with an HTTP POST login request that contains the credentials to authenticate.
-
-
-
Test the setup:
-
In a terminal window, use a
curl
command similar to the following to retrieve an access_token:$ mytoken=$(curl -s \ --user "client-application:password" \ --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail%20employeenumber" \ http://openam.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
-
Validate the access_token returned in the previous step:
$ curl -v http://openig.example.com:8080/rs-pwreplay --header "Authorization: Bearer ${mytoken}"
HTML for the sample application is displayed.
-
Cache Access Tokens
This section builds on the example in Validate Access_Tokens Through the Introspection Endpoint to cache and then revoke access_tokens.
When the access_token is not cached, IG calls AM to validate the access_token. When the access_token is cached, IG doesn’t validate the access_token with AM.
(From AM 6.5.3.) When an access_token is revoked on AM, the CacheAccessTokenResolver can delete the token from the cache when both of the following conditions are true:
-
The
notification
property of AmService is enabled. -
The delegate AccessTokenResolver provides the token metadata required to update the cache.
When a refresh_token is revoked on AM, all associated access_tokens are automatically and immediately revoked.
-
Set up AM as described in Validate Access_Tokens Through the Introspection Endpoint.
-
Set up IG:
-
Set an environment variable for the IG agent password, and then restart IG:
$ export AGENT_SECRET_ID='cGFzc3dvcmQ='
The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.
-
Add the following route to IG:
$HOME/.openig/config/routes/rs-introspect-cache.json
appdata\OpenIG\config\routes\rs-introspect-cache.json
{ "name": "rs-introspect-cache", "baseURI": "http://app.example.com:8081", "condition": "${find(request.uri.path, '^/rs-introspect-cache$')}", "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "AmService-1", "type": "AmService", "config": { "url": "http://openam.example.com:8088/openam", "realm": "/", "version": "7.1", "agent" : { "username" : "ig_agent", "passwordSecretId" : "agent.secret.id" }, "secretsProvider": "SystemAndEnvSecretStore-1" } } ], "handler": { "type": "Chain", "config": { "filters": [ { "name": "OAuth2ResourceServerFilter-1", "type": "OAuth2ResourceServerFilter", "config": { "scopes": [ "mail", "employeenumber" ], "requireHttps": false, "realm": "OpenIG", "accessTokenResolver": { "name": "CacheAccessTokenResolver-1", "type": "CacheAccessTokenResolver", "config": { "enabled": true, "defaultTimeout ": "1 hour", "maximumTimeToCache": "1 day", "amService":"AmService-1", "delegate": { "name": "TokenIntrospectionAccessTokenResolver-1", "type": "TokenIntrospectionAccessTokenResolver", "config": { "amService": "AmService-1", "providerHandler": { "type": "Chain", "config": { "filters": [ { "type": "HttpBasicAuthenticationClientFilter", "config": { "username": "ig_agent", "passwordSecretId": "agent.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1" } } ], "handler": { "type": "Delegate", "capture": "all", "config": { "delegate": "ForgeRockClientHandler" } } } } } } } } } } ], "handler": { "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/html" ] }, "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>" } } } } }
Notice the following features of the route compared to
rs-introspect.json
, in Validate Access_Tokens Through the Introspection Endpoint:-
The OAuth2ResourceServerFilter uses a CacheAccessTokenResolver to cache the access_token, and then delegate token resolution to the TokenIntrospectionAccessTokenResolver.
-
The
amService
property in CacheAccessTokenResolver enables WebSocket notifications from AM, for events such as token revocation. -
The TokenIntrospectionAccessTokenResolver uses a ForgeRockClientHandler and a capture decorator to capture IG’s interactions with AM.
-
-
-
Test token caching:
-
In a terminal window, use a
curl
command similar to the following to retrieve an access_token:$ mytoken=$(curl -s \ --user "client-application:password" \ --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail%20employeenumber" \ http://openam.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
-
Access the route, using the access_token returned in the previous step:
$ curl http://openig.example.com:8080/rs-introspect-cache --header "Authorization: Bearer ${mytoken}" { active = true, scope = employeenumber mail, client_id = client - application, user_id = demo, token_type = Bearer, exp = 158...907, ... }
-
In the route log, note that IG calls AM to introspect the access_token:
-
Access the route again, and in the route log note that this time IG doesn’t call AM, because the token is cached.
-
Disable the cache and repeat the previous steps to cause IG to call AM to validate the access_token for each request.
-
-
Test token revocation:
-
In a terminal window, use a
curl
command similar to the following to revoke the access_token obtained in the previous step:$ curl --request POST \ --data "token=${mytoken}" \ --data "client_id=client-application" \ --data "client_secret=password" \ "http://openam.example.com:8088/openam/oauth2/realms/root/token/revoke"
-
Access the route, using the access_token returned in the previous step, and and note that the request is not authorized because the token is revoked:
$ curl -v http://openig.example.com:8080/rs-introspect-cache --header "Authorization: Bearer ${mytoken}" ... HTTP/1.1 401 Unauthorized
-