Validating 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:
Validating 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".
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
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": "${matches(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.
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, ...
Validating 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
, with the contentpassword
:$
cd $am_keystore_directory
$echo -n password > keystore.pass
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
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": "${matches(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: "Validating 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, ...