Discovering and Dynamically Registering 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 RFC 7591 - 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 "Using 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.
Create a key
/path/to/keystore.jks
:$
keytool -genkey \ -alias myprivatekeyalias \ -keyalg RSA \ -keysize 2048 \ -keystore /path/to/keystore.jks \ -storepass keystore \ -storetype JKS \ -keypass keystore \ -validity 360 \ -dname "CN=openig.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
george
, and change the last name toC0stanza
. 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 the default 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": "${matches(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='george or george@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/keystore.jks
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/keystore.jks", "mappings": [ { "aliases": [ "myprivatekeyalias" ], "secretId": "private.key.jwt.signing.key" } ], "storePassword": "keystore.secret.id", "storeType": "JKS", "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": "${matches(request.uri.path, '^/discovery')}", "handler": { "type": "Chain", "config": { "filters": [ { "name": "DynamicallyRegisteredClient", "type": "OAuth2ClientFilter", "config": { "clientEndpoint": "/discovery", "requireHttps": false, "requireLogin": true, "target": "${attributes.openid}", "failureHandler": { "type": "StaticResponseHandler", "config": { "comment": "Trivial failure handler for debugging only", "status": 500, "reason": "Error", "headers": { "Content-Type": [ "text/plain" ] }, "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://openig.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.sub}" ], "password": [ "${attributes.openid.user_info.family_name}" ] } } } ], "handler": "ReverseProxyHandler" } } }
Consider the differences with
07-openid.json
:The route matches requests to
/discovery
.The OAuth2ClientFilter 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, after the process is complete and IG has injected the OpenID Connect user information into the context.
Test the setup:
Log out of AM.
Enter the following email address:
george@example.com
. The AM login page is displayed.Log in as user
george
, passwordC0stanza
, and then allow the application to access user information. The sample application returns George's page.