OpenID Connect
The following sections provide an overview of how IG supports OpenID Connect 1.0, and examples of to set up IG as an OpenID Connect relying party in different deployment scenarios:
About IG with OpenID Connect
IG supports OpenID Connect 1.0, an authentication layer built on OAuth 2.0. OpenID Connect 1.0 is a specific implementation of OAuth 2.0, where the identity provider holds the protected resource that the third-party application wants to access. For more information, refer to OpenID Connect.
OpenID Connect refers to the following entities:
-
End user : An OAuth 2.0 resource owner whose user information the application needs to access.
The end user wants to use an application through an existing identity provider account without signing up and creating credentials for another web service.
-
Relying Party (RP): An OAuth 2.0 client that needs access to the end user’s protected user information.
For example, an online mail application needs to know which end user is accessing the application in order to present the correct inbox.
As another example, an online shopping site needs to know which end user is accessing the site in order to present the right offerings, account, and shopping cart.
-
OpenID Provider (OP): An OAuth 2.0 authorization server and also resource server that holds the user information and grants access.
The OP requires the end user to give the RP permission to access to some of its user information. Because OpenID Connect 1.0 defines unique identification for an account (subject identifier + issuer identifier), the RP can use that identification to bind its own user profile to a remote identity.
For the online mail application, this key could be used to access the mailboxes and related account information. For the online shopping site, this key could be used to access the offerings, account, shopping cart and others. The key makes it possible to serve users as if they had local accounts.
-
UserInfo : The protected resource that the third-party application wants to access. The information about the authenticated end user is expressed in a standard format. The user-info endpoint is hosted on the authorization server and is protected with OAuth 2.0.
When IG acts as an OpenID Connect relying party, its role is to retrieve user information from the OpenID provider, and then to inject that information into the context for use by subsequent filters and handlers.
Use AM as a single OpenID Connect provider
This section gives an example of how to set up AM as an OpenID Connect identity provider, and IG as a relying party for browser requests to the home page of the sample application.
The following sequence diagram shows the flow of information for a request to access the home page of the sample application, using AM as a single, preregistered OpenID Connect identity provider, and IG as a relying party:
Before you start, prepare AM, IG, and the sample application as described in Example installation for this guide.
-
Set Up AM as an OpenID Connect provider:
-
(From AM 6.5.3) Select Services > Add a Service, and add a Validation Service with the following Valid goto URL Resources:
-
http://ig.example.com:8080/*
-
http://ig.example.com:8080/*?*
-
-
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.
-
Add a client with the following values:
-
Client ID:
oidc_client
-
Client secret:
password
-
Redirection URIs:
http://ig.example.com:8080/home/id_token/callback
-
Scope(s):
openid
,profile
, andemail
-
-
(From AM 6.5) On the Advanced tab, select the following values:
-
Grant Types:
Authorization Code
-
-
On the Signing and Encryption tab, change ID Token Signing Algorithm to
HS256
,HS384
, orHS512
. The algorithm must be HMAC.
-
-
-
Set up IG:
-
Set an environment variable for
oidc_client
, and then restart IG:$ export OIDC_SECRET_ID='cGFzc3dvcmQ='
-
Add the following route to IG, to serve .css and other static resources for the sample application:
$HOME/.openig/config/routes/static-resources.json
appdata\OpenIG\config\routes\static-resources.json
{ "name" : "sampleapp-resources", "baseURI" : "http://app.example.com:8081", "condition": "${find(request.uri.path,'^/css')}", "handler": "ReverseProxyHandler" }
-
Add the following route to IG:
$HOME/.openig/config/routes/07-openid.json
appdata\OpenIG\config\routes\07-openid.json
{ "name": "07-openid", "baseURI": "http://app.example.com:8081", "condition": "${find(request.uri.path, '^/home/id_token')}", "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "AuthenticatedRegistrationHandler-1", "type": "Chain", "config": { "filters": [ { "name": "ClientSecretBasicAuthenticationFilter-1", "type": "ClientSecretBasicAuthenticationFilter", "config": { "clientId": "oidc_client", "clientSecretId": "oidc.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1" } } ], "handler": "ForgeRockClientHandler" } } ], "handler": { "type": "Chain", "config": { "filters": [ { "name": "AuthorizationCodeOAuth2ClientFilter-1", "type": "AuthorizationCodeOAuth2ClientFilter", "config": { "clientEndpoint": "/home/id_token", "failureHandler": { "type": "StaticResponseHandler", "config": { "status": 500, "headers": { "Content-Type": [ "text/plain" ] }, "entity": "Error in OAuth 2.0 setup." } }, "registrations": [ { "name": "oidc-user-info-client", "type": "ClientRegistration", "config": { "clientId": "oidc_client", "issuer": { "name": "Issuer", "type": "Issuer", "config": { "wellKnownEndpoint": "http://am.example.com:8088/openam/oauth2/.well-known/openid-configuration" } }, "scopes": [ "openid", "profile", "email" ], "authenticatedRegistrationHandler": "AuthenticatedRegistrationHandler-1" } } ], "requireHttps": false, "cacheExpiration": "disabled" } } ], "handler": "ReverseProxyHandler" } } }
For information about how to set up the IG route in Studio, see OpenID Connect in Structured Editor.
Notice the following features about the route:
-
The route matches requests to
/home/id_token
. -
The
AuthorizationCodeOAuth2ClientFilter
enables IG to act as a relying party. It uses a single client registration that is defined inline and refers to the AM server configured in Use AM As a single OpenID Connect provider. -
The filter has a base client endpoint of
/home/id_token
, which creates the following service URIs:-
Requests to
/home/id_token/login
start the delegated authorization process. -
Requests to
/home/id_token/callback
are expected as redirects from the OAuth 2.0 Authorization Server (OpenID Connect provider). This is why the redirect URI in the client profile in AM is set tohttp://ig.example.com:8080/home/id_token/callback
. -
Requests to
/home/id_token/logout
remove the authorization state for the end user, and redirect to the specified URL if agoto
parameter is provided.These endpoints are implicitly reserved. Attempts to access them directly can cause undefined errors.
-
-
For convenience in this test,
"requireHttps"
is false. In production environments, set it to true. So that you see the delegated authorization process when you make a request,"requireLogin"
has the default valuetrue
. -
The target for storing authorization state information is
${attributes.openid}
. This is where subsequent filters and handlers can find access tokens and user information.
-
-
-
Test the setup:
-
If you are logged in to AM, log out and clear any cookies.
-
Go to http://ig.example.com:8080/home/id_token.
The AM login page is displayed.
-
Log in to AM as user
demo
, passwordCh4ng31t
, and then allow the application to access user information.The home page of the sample application is displayed.
-
Authenticate automatically to the sample application
To authenticate automatically to the sample application, change the last
name of the user demo
to match the password Ch4ng31t
, and
add a StaticRequestFilter like the following to the end of the chain in
07-openid.json
:
{
"type": "StaticRequestFilter",
"config": {
"method": "POST",
"uri": "http://app.example.com:8081/login",
"form": {
"username": [
"${attributes.openid.user_info.sub}"
],
"password": [
"${attributes.openid.user_info.family_name}"
]
}
}
}
The StaticRequestFilter retrieves the username and password from the context, and replaces the original HTTP GET request with an HTTP POST login request containing credentials.
Use multiple OpenID Connect providers
This section builds on the example in Use AM As a single OpenID Connect provider to give an example of using OpenID Connect with two identity providers.
The client registration for the AM provider is declared in the heap, and a second client registration defines Google as an alternative identity provider. The Nascar page helps the user to choose an identity provider.
-
Set up AM as the first OpenID Connect provider, as described in Use AM As a single OpenID Connect provider.
-
Set up Google as the second OpenID Connect identity provider, using the following hints:
-
Create credentials for an OAuth 2.0 client ID with the following options:
-
Application type:
Web application
-
Authorized redirect URI:
http://ig.example.com:8080/home/id_token/callback
-
-
Make a note of the ID and password for the Google identity provider.
-
Set up IG:
-
Add the following route to IG, to serve .css and other static resources for the sample application:
$HOME/.openig/config/routes/static-resources.json
appdata\OpenIG\config\routes\static-resources.json
{ "name" : "sampleapp-resources", "baseURI" : "http://app.example.com:8081", "condition": "${find(request.uri.path,'^/css')}", "handler": "ReverseProxyHandler" }
-
Add the following route to IG:
$HOME/.openig/config/routes/07-openid-nascar.json
appdata\OpenIG\config\routes\07-openid-nascar.json
{ "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "openam", "type": "ClientRegistration", "config": { "clientId": "oidc_client", "clientSecretId": "oidc.secret.id", "issuer": { "name": "Issuer", "type": "Issuer", "config": { "wellKnownEndpoint": "http://am.example.com:8088/openam/oauth2/.well-known/openid-configuration" } }, "scopes": [ "openid", "profile", "email" ], "secretsProvider": "SystemAndEnvSecretStore-1", "tokenEndpointAuthMethod": "client_secret_basic" } }, { "name": "google", "type": "ClientRegistration", "config": { "clientId": "googleClientId", "clientSecretId": "google.secret.id", "issuer": { "name": "accounts.google.com", "type": "Issuer", "config": { "wellKnownEndpoint": "https://accounts.google.com/.well-known/openid-configuration" } }, "scopes": [ "openid", "profile" ], "secretsProvider": "SystemAndEnvSecretStore-1" } }, { "name": "NascarPage", "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/html; charset=UTF-8" ] }, "entity": [ "<html>", " <body>", " <p><a href='/home/id_token/login?registration=oidc_client&issuer=Issuer&goto=${urlEncodeQueryParameterNameOrValue('http://ig.example.com:8080/home/id_token')}'>AM Login</a></p>", " <p><a href='/home/id_token/login?registration=googleClientId&issuer=accounts.google.com&goto=${urlEncodeQueryParameterNameOrValue('http://ig.example.com:8080/home/id_token')}'>Google Login</a></p>", " </body>", "</html>" ] } } ], "name": "07-openid-nascar", "baseURI": "http://app.example.com:8081", "condition": "${find(request.uri.path, '^/home/id_token')}", "handler": { "type": "Chain", "config": { "filters": [ { "type": "AuthorizationCodeOAuth2ClientFilter", "config": { "clientEndpoint": "/home/id_token", "failureHandler": { "type": "StaticResponseHandler", "config": { "comment": "Trivial failure handler for debugging only", "status": 500, "headers": { "Content-Type": [ "text/plain; charset=UTF-8" ] }, "entity": "${attributes.openid}" } }, "loginHandler": "NascarPage", "registrations": [ "openam", "google" ], "requireHttps": false, "cacheExpiration": "disabled" } } ], "handler": "ReverseProxyHandler" } } }
Consider the differences with
07-openid.json
:-
The heap objects
openam
andgoogle
define two client registrations to authenticate IG to identity providers. -
The heap object
NascarPage
is a StaticResponseHandler that provides links to the two client registrations. -
The AuthorizationCodeOAuth2ClientFilter uses a
loginHandler
that refers toNascarPage
to allow users to choose from the two client registrations.
-
-
In the route, replace both occurrences of
googleClientId
by the Google identity provider ID. -
Set environment variables for the identity providers' passwords:
-
Set an environment variable for the password of the AM identity provider,
oidc_client
:$ export OIDC.SECRET.ID='cGFzc3dvcmQ='
-
Set an environment variable for the password of the Google identity provider:
$ export GOOGLE.SECRET.ID='base64-encoded-google-client-password'
The passwords are retrieved by the SystemAndEnvSecretStore, and must be base64-encoded.
-
-
-
Test the setup:
-
Log out of AM.
-
Go to http://ig.example.com:8080/home/id_token.
The Nascar page offers the choice of identity provider.
-
Select a provider, log in with your credentials, and then allow the application to access user information.
For AM, use the following credentials: username
demo
, passwordCh4ng31t
. For the Google identity provider, use the Google credentials.The home page of the sample application is displayed.
-
Discover and dynamically register with OpenID Connect providers
OpenID Connect defines mechanisms for discovering and dynamically registering with an identity provider that is not known in advance, as specified in the following publications: OpenID Connect Discovery, OpenID Connect Dynamic Client Registration, and OAuth 2.0 Dynamic Client Registration Protocol.
In dynamic registration, issuer and client registrations are generated dynamically. They are held in memory and can be reused, but do not persist when IG is restarted.
This section builds on the example in Use AM As a single OpenID Connect provider to give an example of discovering and dynamically registering with an identity provider that is not known in advance. In this example, the client sends a signed JWT to the authorization server.
To facilitate the example, a WebFinger service is embedded in the sample application. In a normal deployment, the WebFinger server is likely to be a service on the issuer’s domain.
-
Set up a key
-
Locate a directory for secrets, and go to it:
$ cd /path/to/secrets
-
Create a key:
$ keytool -genkey \ -alias myprivatekeyalias \ -keyalg RSA \ -keysize 2048 \ -keystore keystore.p12 \ -storepass keystore \ -storetype PKCS12 \ -keypass keystore \ -validity 360 \ -dname "CN=ig.example.com, OU=example, O=com, L=fr, ST=fr, C=fr"
-
-
Set up AM:
-
Set up AM as described in Use AM As a single OpenID Connect provider.
-
Select the user
demo
, and change the last name toCh4ng31t
. Note that, for this example, the last name must be the same as the password. -
Configure the OAuth 2.0 Authorization Server for dynamic registration:
-
Select Services > OAuth2 Provider.
-
On the Advanced tab, add the following scopes to Client Registration Scope Whitelist:
openid
,profile
,email
. -
On the Client Dynamic Registration tab, select these settings:
-
Allow Open Dynamic Client Registration: Enabled
-
Generate Registration Access Tokens: Disabled
-
-
-
Configure the authentication method for the OAuth 2.0 Client:
-
Select Applications > OAuth 2.0 > Clients.
-
Select
oidc_client
, and on the Advanced tab, select Token Endpoint Authentication Method:private_key_jwt
.
-
-
-
Set up IG:
-
In the IG configuration, set an environment variable for the keystore password, and then restart IG:
$ export KEYSTORE_SECRET_ID='a2V5c3RvcmU='
The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.
-
Add the following route to IG, to serve .css and other static resources for the sample application:
$HOME/.openig/config/routes/static-resources.json
appdata\OpenIG\config\routes\static-resources.json
{ "name" : "sampleapp-resources", "baseURI" : "http://app.example.com:8081", "condition": "${find(request.uri.path,'^/css')}", "handler": "ReverseProxyHandler" }
-
Add the following script to IG:
$HOME/.openig/scripts/groovy/discovery.groovy
appdata\OpenIG\scripts\groovy\discovery.groovy
/* * OIDC discovery with the sample application */ response = new Response(Status.OK) response.getHeaders().put(ContentTypeHeader.NAME, "text/html"); response.entity = """ <!doctype html> <html> <head> <title>OpenID Connect Discovery</title> <meta charset='UTF-8'> </head> <body> <form id='form' action='/discovery/login?'> Enter your user ID or email address: <input type='text' id='discovery' name='discovery' placeholder='demo or demo@example.com' /> <input type='hidden' name='goto' value='${contexts.router.originalUri}' /> </form> <script> // Make sure sampleAppUrl is correct for your sample app. window.onload = function() { document.getElementById('form').onsubmit = function() { // Fix the URL if not using the default settings. var sampleAppUrl = 'http://app.example.com:8081/'; var discovery = document.getElementById('discovery'); discovery.value = sampleAppUrl + discovery.value.split('@', 1)[0]; }; }; </script> </body> </html>""" as String return response
The script transforms the input into a
discovery
value for IG. This is not a requirement for deployment, only a convenience for the purposes of this example. Alternatives are described in the discovery protocol specification. -
Add the following route to IG, replacing
/path/to/secrets/keystore.p12
with your path:$HOME/.openig/config/routes/07-discovery.json
appdata\OpenIG\config\routes\07-discovery.json
{ "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "SecretsProvider-1", "type": "SecretsProvider", "config": { "stores": [ { "type": "KeyStoreSecretStore", "config": { "file": "/path/to/secrets/keystore.p12", "mappings": [ { "aliases": [ "myprivatekeyalias" ], "secretId": "private.key.jwt.signing.key" } ], "storePasswordSecretId": "keystore.secret.id", "storeType": "PKCS12", "secretsProvider": "SystemAndEnvSecretStore-1" } } ] } }, { "name": "DiscoveryPage", "type": "ScriptableHandler", "config": { "type": "application/x-groovy", "file": "discovery.groovy" } } ], "name": "07-discovery", "baseURI": "http://app.example.com:8081", "condition": "${find(request.uri.path, '^/discovery')}", "handler": { "type": "Chain", "config": { "filters": [ { "name": "DynamicallyRegisteredClient", "type": "AuthorizationCodeOAuth2ClientFilter", "config": { "clientEndpoint": "/discovery", "requireHttps": false, "requireLogin": true, "target": "${attributes.openid}", "failureHandler": { "type": "StaticResponseHandler", "config": { "comment": "Trivial failure handler for debugging only", "status": 500, "headers": { "Content-Type": [ "text/plain; charset=UTF-8" ] }, "entity": "${attributes.openid}" } }, "loginHandler": "DiscoveryPage", "discoverySecretId": "private.key.jwt.signing.key", "tokenEndpointAuthMethod": "private_key_jwt", "secretsProvider": "SecretsProvider-1", "metadata": { "client_name": "My Dynamically Registered Client", "redirect_uris": [ "http://ig.example.com:8080/discovery/callback" ], "scopes": [ "openid", "profile", "email" ] } } }, { "type": "StaticRequestFilter", "config": { "method": "POST", "uri": "http://app.example.com:8081/login", "form": { "username": [ "${attributes.openid.user_info.name}" ], "password": [ "${attributes.openid.user_info.family_name}" ] } } } ], "handler": "ReverseProxyHandler" } } }
Consider the differences with
07-openid.json
:-
The route matches requests to
/discovery
. -
The AuthorizationCodeOAuth2ClientFilter uses
DiscoveryPage
as the login handler, and specifies metadata to prepare the dynamic registration request. -
DiscoveryPage
uses a ScriptableHandler and script to provide thediscovery
parameter andgoto
parameter.If there is a match, then it can use the issuer’s registration endpoint and avoid an additional request to look up the user’s issuer using the WebFinger protocol.
If there is no match, IG uses the
discovery
value as theresource
for a WebFinger request using OpenID Connect Discovery protocol. -
IG uses the
discovery
parameter to find an identity provider. IG extracts the domain host and port from the value, and attempts to find a match in thesupportedDomains
lists for issuers configured for the route. -
When
discoverySecretId
is set, thetokenEndpointAuthMethod
is alwaysprivate_key_jwt
. Clients send a signed JWT to the authorization server.Redirects IG to the end user’s browser, using the
goto
parameter, fter the process is complete and IG has injected the OpenID Connect user information into the context.
-
-
-
Test the setup:
-
Log out of AM, and clear any cookies.
-
Enter the following email address:
demo@example.com
. The AM login page is displayed. -
Log in as user
demo
, passwordCh4ng31t
, and then allow the application to access user information. The sample application returns the user’s page.
-