PingGateway 2024.9

Discovery and dynamic registration

OIDC defines mechanisms for discovering and dynamically registering with an identity provider that isn’t 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 don’t persist when PingGateway is restarted.

This section builds on the example in AM as OIDC provider to give an example of discovering and dynamically registering with an identity provider that isn’t 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.

  1. Set up a key

    1. Locate a directory for secrets, and go to it:

      $ cd /path/to/secrets
    2. 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"
  2. Set up AM:

    1. Set up AM as described in AM as OIDC provider.

    2. Select the user demo, and change the last name to Ch4ng31t. 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 Allowlist: 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 PingGateway:

    1. In the PingGateway configuration, set an environment variable for the keystore password, and then restart PingGateway:

      $ export KEYSTORE_SECRET_ID='a2V5c3RvcmU='

      The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.

    2. Add the following route to PingGateway to serve the sample application .css and other static resources:

      • Linux

      • Windows

      $HOME/.openig/config/routes/00-static-resources.json
      %appdata%\OpenIG\config\routes\00-static-resources.json
      {
        "name" : "00-static-resources",
        "baseURI" : "http://app.example.com:8081",
        "condition": "${find(request.uri.path,'^/css') or matchesWithRegex(request.uri.path, '^/.*\\\\.ico$') or matchesWithRegex(request.uri.path, '^/.*\\\\.gif$')}",
        "handler": "ReverseProxyHandler"
      }
    3. Add the following script to PingGateway:

      • Linux

      • Windows

      $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.idpSelectionLogin.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 PingGateway. 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 PingGateway, replacing /path/to/secrets/keystore.p12 with your path:

      • Linux

      • Windows

      $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": "${contexts.oauth2Failure.error}: ${contexts.oauth2Failure.description}"
                    }
                  },
                  "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 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, PingGateway uses the discovery value as the resource for a WebFinger request using the OIDC discovery protocol.

      • PingGateway uses the discovery parameter to find an identity provider. PingGateway 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 PingGateway to the end user’s browser, using the goto parameter, after the process is complete and PingGateway has injected the OIDC user information into the context.

  4. Test the setup:

    1. Log out of AM, and clear any cookies.

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

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

    4. Log in as user demo, password Ch4ng31t, and then allow the application to access user information. The sample application returns the user’s page.