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.

Dynamic Registration With OpenID Connect Providers
  1. 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"
  2. Set up AM:

    1. Set up AM as described in "Use AM As a Single OpenID Connect Provider".

    2. Select the user george, and change the last name to C0stanza. Note that, for this example, the last name must be the same as the password.

    3. Configure the OAuth 2.0 Authorization Server for dynamic registration:

      1. Select  Services > OAuth2 Provider.

      2. On the Advanced tab, add the following scopes to Client Registration Scope Whitelist: openid, profile, email.

      3. On the Client Dynamic Registration tab, select these settings:

        • Allow Open Dynamic Client Registration: Enabled

        • Generate Registration Access Tokens: Disabled

    4. Configure the authentication method for the OAuth 2.0 Client:

      1. Select Applications > OAuth 2.0 > Clients.

      2. Select oidc_client, and on the Advanced tab, select Token Endpoint Authentication Method: private_key_jwt.

  3. Set up IG:

    1. 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.

    2. 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"
      }
    3. 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.

    4. 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 the discovery parameter and goto 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 the resource 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 the supportedDomains lists for issuers configured for the route.

      • When discoverySecretId is set, the tokenEndpointAuthMethod is always private_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.

  4. Test the setup:

    1. Log out of AM.

    2. Go to http://openig.example.com:8080/discovery.

    3. Enter the following email address: george@example.com. The AM login page is displayed.

    4. Log in as user george, password C0stanza, and then allow the application to access user information. The sample application returns George's page.

Read a different version of :