PingGateway 2024.6

OpenID Connect

The following sections provide an overview of how PingGateway supports OpenID Connect 1.0, and examples of to set up PingGateway as an OpenID Connect relying party in different deployment scenarios:

About PingGateway with OpenID Connect

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

AM as an OpenID Connect provider

This section gives an example of how to set up AM as an OpenID Connect identity provider, and PingGateway 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 PingGateway as a relying party:

oidc-am

Before you start, prepare AM, PingGateway, and the sample application as described in Example installation for this guide.

  1. Set Up AM as an OpenID Connect provider:

    1. Select Services > Add a Service and add a Validation Service with the following Valid goto URL Resources:

      • https://ig.example.com:8443/*

      • https://ig.example.com:8443/*?*

    2. Create an OAuth 2.0 Authorization Server:

      1. Select Services > Add a Service > OAuth2 Provider.

      2. Add a service with the default values.

    3. Create an OAuth 2.0 Client to request OAuth 2.0 access tokens:

      1. Select Applications > OAuth 2.0 > Clients.

      2. Add a client with the following values:

        • Client ID: oidc_client

        • Client secret: password

        • Redirection URIs: https://ig.example.com:8443/home/id_token/callback

        • Scope(s): openid, profile, and email

      3. On the Advanced tab, select the following values:

        • Grant Types: Authorization Code

  2. Set up PingGateway:

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

    2. Set an environment variable for oidc_client, and then restart PingGateway:

      $ export OIDC_SECRET_ID='cGFzc3dvcmQ='
    3. 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"
      }
    4. Add the following route to PingGateway:

      • Linux

      • Windows

      $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 PingGateway 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 PingGateway to act as a relying party. It uses a single client registration that is defined inline and refers to the AM server configured in 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 to https://ig.example.com:8443/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 a goto 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 value true.

      • The target for storing authorization state information is ${attributes.openid}. This is where subsequent filters and handlers can find access tokens and user information.

  3. Test the setup:

    1. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/home/id_token.

      The AM login page is displayed.

    2. Log in to AM as user demo, password Ch4ng31t, 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.

PingOne Advanced Identity Cloud as an OpenID Connect provider

This example sets up PingOne Advanced Identity Cloud as an OpenID Connect identity provider, and PingGateway as a relying party.

For more information about PingGateway and OpenID Connect, refer to OpenID Connect.

Before you start, prepare PingOne Advanced Identity Cloud, PingGateway, and the sample application as described in Example installation for this guide.

  1. Set up PingOne Advanced Identity Cloud:

    1. Log in to the PingOne Advanced Identity Cloud admin UI as an administrator.

    2. Make sure you are managing the alpha realm. If not, click the current realm at the top of the screen, and switch realm.

    3. Go to group Identities > Manage > settings_system_daydream Alpha realm - Users, and add a user with the following values:

      • Username: demo

      • First name: demo

      • Last name: user

      • Email Address: demo@example.com

      • Password: Ch4ng3!t

    4. Go to Applications > CustomApplication > OIDC - OpenId Connect > Web and add a web application with the following values:

      • Name: oidc_client

      • Owners: demo user

      • Client Secret: password

      • Sign On > Sign-in URLs: https://ig.example.com:8443/home/id_token/callback

      • Sign On > Grant Types: Authorization Code

      • Sign On > Scopes: openid, profile, email

      • Show advanced settings > Authentication > Implied Consent: On

    For more information, refer to PingOne Advanced Identity Cloud’s Application management.

  2. Set up PingGateway:

    1. Set an environment variable for the oidc_client password, and then restart PingGateway:

      $ export OIDC_SECRET_ID='cGFzc3dvcmQ='
    1. 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"
      }
    2. Add the following route to PingGateway, replacing the value for the property amInstanceUrl:

      • Linux

      • Windows

      $HOME/.openig/config/routes/oidc-idc.json
      %appdata%\OpenIG\config\routes\oidc-idc.json
      {
        "name": "oidc-idc",
        "baseURI": "http://app.example.com:8081",
        "condition": "${find(request.uri.path, '^/home/id_token')}",
        "properties": {
          "amInstanceUrl": "https://myTenant.forgeblocks.com/am"
        },
        "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": "oauth2-client",
                      "type": "ClientRegistration",
                      "config": {
                        "clientId": "oidc_client",
                        "issuer": {
                          "name": "Issuer",
                          "type": "Issuer",
                          "config": {
                            "wellKnownEndpoint": "&{amInstanceUrl}/oauth2/realms/alpha/.well-known/openid-configuration"
                          }
                        },
                        "scopes": [
                          "openid",
                          "profile",
                          "email"
                        ],
                        "authenticatedRegistrationHandler": "AuthenticatedRegistrationHandler-1"
                      }
                    }
                  ],
                  "requireHttps": false,
                  "cacheExpiration": "disabled"
                }
              }
            ],
            "handler": "ReverseProxyHandler"
          }
        }
      }

      Compared to 07-openid.json in AM as a single OpenID Connect provider, where PingAM is running locally, the ClientRegistration wellKnownEndpoint points to PingOne Advanced Identity Cloud.

  3. Test the setup:

    1. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/home/id_token.

      The PingOne Advanced Identity Cloud login page is displayed.

    2. Log in to PingOne Advanced Identity Cloud as user demo, password Ch4ng3!t. The home page of the sample application is displayed.

PingOne as an OpenID Connect provider

This example sets up PingOne as an OpenID Connect identity provider and PingGateway as a relying party.

Before you start, prepare PingGateway and the sample application as described in the Quick install.

  1. Set up the PingOne environment:

  2. Create a PingOne OIDC web application.

    Learn more from PingOne’s Creating a web application.

    1. In the test environment, create a web application with the following values:

      • Application Name: oidc_client

      • Description: OIDC client

      • Application Type: OIDC Web App

    2. In the application, select the Overview panel and click Protocol OpenID Connect.

    3. In the Redirect URIs field, add https://ig.example.com:8443/home/id_token/callback and then save the application.

      Learn more from PingOne’s Editing an application - OIDC.

    4. At the top-right of the page, click the slider to enable the application.

    5. Go to the Configuration panel and make a note of the following values in the URLs drop-down list:

      • OIDC Discovery Endpoint

      • Client ID

      • Client Secret

        The values are used in the PingGateway setup.

  3. Set up PingGateway:

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

    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. Base64-encode the Client Secret for the web application created in the previous step, and then set the value as an environment variable:

      $ export OIDC_SECRET_ID='Yy5...A=='
    4. Add the following route to PingGateway, replacing the values of the following properties with values for the web application created in the previous step:

      • OIDC_Discovery_Endpoint: OIDC Discovery Endpoint

      • Client_ID: Client ID

        • Linux

        • Windows

        $HOME/.openig/config/routes/oidc-ping.json
        %appdata%\OpenIG\config\routes\oidc-ping.json
        {
          "name": "oidc-ping",
          "baseURI": "http://app.example.com:8081",
          "condition": "${find(request.uri.path, '^/home/id_token')}",
          "properties": {
            "OIDC_Discovery_Endpoint": "OIDC Discovery endpoint of the web app",
            "Client_ID": "Client ID of the web app"
          },
          "heap": [
            {
              "name": "SystemAndEnvSecretStore-1",
              "type": "SystemAndEnvSecretStore"
            },
            {
              "name": "AuthenticatedRegistrationHandler-1",
              "type": "Chain",
              "config": {
                "filters": [
                  {
                    "name": "ClientSecretBasicAuthenticationFilter-1",
                    "type": "ClientSecretBasicAuthenticationFilter",
                    "config": {
                      "clientId": "&{Client_ID}",
                      "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/html; charset=UTF-8" ]
                        },
                        "entity": "<html><body>Error in OAuth 2.0 setup.<br> ${contexts.oauth2Failure.exception.message}</body></html>"
                      }
                    },
                    "registrations": [
                      {
                        "name": "oauth2-client",
                        "type": "ClientRegistration",
                        "config": {
                          "clientId": "${Client_ID}",
                          "issuer": {
                            "name": "PingOne",
                            "type": "Issuer",
                            "config": {
                              "wellKnownEndpoint": "&{OIDC_Discovery_Endpoint}"
                            }
                          },
                          "scopes": [
                            "openid"
                          ],
                          "authenticatedRegistrationHandler": "AuthenticatedRegistrationHandler-1"
                        }
                      }
                    ],
                    "requireHttps": false,
                    "cacheExpiration": "disabled"
                  }
                }
              ],
              "handler": "ReverseProxyHandler"
            }
          }
        }
    5. Restart PingGateway.

  4. Test the setup:

    1. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/home/id_token.

      The PingOne login page is displayed.

    2. Log in to PingOne as user demo, password Ch4ng3!t.

    3. If prompted by PingOne, change the password of the demo user.

      The home page of the sample application is displayed.

Multiple OpenID Connect providers

This section gives an example of using OpenID Connect with two identity providers.

Client registrations for an AM identity provider and PingOne Advanced Identity Cloud identity provider are declared in the heap. The Nascar page helps the user to choose an identity provider.

  1. Set up AM as the first identity provider, as described in AM as a single OpenID Connect provider.

  2. Set up PingOne Advanced Identity Cloud as a second identity provider, as described in PingOne Advanced Identity Cloud as an OpenID Connect provider.

  3. Add the following route to PingGateway, replacing the value for the property amInstanceUrl:

    • Linux

    • Windows

    $HOME/.openig/config/routes/07-openid-nascar.json
    %appdata%\OpenIG\config\routes\07-openid-nascar.json
    {
      "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"
          }
        },
        {
          "name": "openam",
          "type": "ClientRegistration",
          "config": {
            "clientId": "oidc_client",
            "issuer": {
              "name": "am_issuer",
              "type": "Issuer",
              "config": {
                "wellKnownEndpoint": "http://am.example.com:8088/openam/oauth2/.well-known/openid-configuration"
              }
            },
            "scopes": [
              "openid",
              "profile",
              "email"
            ],
            "authenticatedRegistrationHandler": "AuthenticatedRegistrationHandler-1"
          }
        },
        {
          "name": "idcloud",
          "type": "ClientRegistration",
          "config": {
            "clientId": "oidc_client",
            "issuer": {
              "name": "idc_issuer",
              "type": "Issuer",
              "config": {
                "wellKnownEndpoint": "&{amInstanceUrl}/oauth2/realms/alpha/.well-known/openid-configuration"
              }
            },
            "scopes": [
              "openid",
              "profile",
              "email"
            ],
            "authenticatedRegistrationHandler": "AuthenticatedRegistrationHandler-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=am_issuer&goto=${urlEncodeQueryParameterNameOrValue('https://ig.example.com:8443/home/id_token')}'>Access Management login</a></p>",
              "    <p><a href='/home/id_token/login?registration=oidc_client&issuer=idc_issuer&goto=${urlEncodeQueryParameterNameOrValue('https://ig.example.com:8443/home/id_token')}'>Identity Cloud login</a></p>",
              "  </body>",
              "</html>"
            ]
          }
        }
      ],
      "name": "07-openid-nascar",
      "baseURI": "http://app.example.com:8081",
      "condition": "${find(request.uri.path, '^/home/id_token')}",
      "properties": {
        "amInstanceUrl": "https://myTenant.forgeblocks.com/am"
      },
      "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": "${contexts.oauth2Failure.error}: ${contexts.oauth2Failure.description}"
                  }
                },
                "loginHandler": "NascarPage",
                "registrations": [ "openam", "idcloud" ],
                "requireHttps": false,
                "cacheExpiration": "disabled"
              }
            }
          ],
          "handler": "ReverseProxyHandler"
        }
      }
    }

    Consider the differences with 07-openid.json:

    • The heap objects openam and idcloud define client registrations.

    • The StaticResponseHandler provides links to the client registrations.

    • The AuthorizationCodeOAuth2ClientFilter uses a loginHandler to allow users to choose a client registration and therefore an identity provider.

  4. Test the setup:

    1. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/home/id_token.

      The Nascar page offers the choice of identity provider.

    2. Using the following credentials, select a provider, log in, and allow the application to access user information:

      • AM: user demo, password Ch4ng31t.

      • PingOne Advanced Identity Cloud: user demo, password Ch4ng3!t

        The home page of the sample application is displayed.

Discovery and dynamic registration with OpenID Connect providers

OpenID Connect 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 a single OpenID Connect 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 a single OpenID Connect 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 OpenID Connect 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 OpenID Connect 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.

ID token validation

This section uses an IdTokenValidationFilter to validate an ID token.

  1. Set up AM:

    1. Set up AM as described in Validate access tokens through the introspection endpoint.

    2. Select Applications > OAuth 2.0 > Clients and add the additional scope openid to client-application.

  2. Set up PingGateway:

    1. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/idtokenvalidation.json
      %appdata%\OpenIG\config\routes\idtokenvalidation.json
      {
        "name": "idtokenvalidation",
        "condition": "${find(request.uri.path, '^/idtokenvalidation')}",
        "capture": "all",
        "handler": {
          "type": "Chain",
          "config": {
            "filters": [{
              "type": "IdTokenValidationFilter",
              "config": {
                "idToken": "<id_token_value>",
                "audience": "client-application",
                "issuer": "http://am.example.com:8088/openam/oauth2",
                "failureHandler": {
                  "type": "ScriptableHandler",
                  "config": {
                    "type": "application/x-groovy",
                    "source": [
                      "def response = new Response(Status.FORBIDDEN)",
                      "response.headers['Content-Type'] = 'text/html; charset=utf-8'",
                      "def errors = contexts.jwtValidationError.violations.collect{it.description}",
                      "def display = \"<html>Can't validate id_token:<br> ${contexts.jwtValidationError.jwt} \"",
                      "display <<=\"<br><br>For the following errors:<br> ${errors.join(\"<br>\")}</html>\"",
                      "response.entity=display as String",
                      "return response"
                    ]
                  }
                },
                "verificationSecretId": "verify",
                "secretsProvider": {
                  "type": "JwkSetSecretStore",
                  "config": {
                    "jwkUrl": "http://am.example.com:8088/openam/oauth2/connect/jwk_uri"
                  }
                }
              }
            }],
            "handler": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "Content-Type": [ "text/html; charset=UTF-8" ]
                },
                "entity": "<html><body>Validated id_token:<br> ${contexts.jwtValidation.value}</body></html>"
              }
            }
          }
        }
      }

      Notice the following features of the route:

      • The route matches requests to /idtokenvalidation.

      • A SecretsProvider declares a JwkSetSecretStore to validate secrets for signed JWTs. The JwkSetSecretStore specifies a URL to a JWK set on AM that contains the signing keys.

      • The property verificationSecretId is configured with an arbitrary value. If this property isn’t configured, the filter doesn’t verify the signature of tokens.

      • The JwkSetSecretStore specifies the URL to a JWK set on AM, that contains verification keys identified by a kid. PingGateway validates the token signature as follows:

        • If the value of a kid in the JWK set matches a kid in the the signed JWT, the JwkSetSecretStore verifies the signature.

        • If the JWT doesn’t have a kid, or if the JWK set doesn’t contain a key with the same value, the JwkSetSecretStore looks for valid secrets with the same purpose as the value of verificationSecretId.

      • If the filter validates the token, the StaticResponseHandler displays the token value from the context ${contexts.jwtValidation.value}. Otherwise, the ScriptableHandler displays the token value and a list of violations from the context ${contexts.jwtValidationError.violations}

  3. Test the setup:

    1. In a terminal window, use a curl command similar to the following to retrieve an id_token:

      $ curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=openid" \
      http://am.example.com:8088/openam/oauth2/access_token
      
      {
       "access_token":"...",
       "scope":"openid",
       "id_token":"...",
       "token_type":"Bearer",
       "expires_in":3599
      }
    2. In the route, replace <id_token_value> with the value of the id_token returned in the previous step.

    3. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/idtokenvalidation.

      The validated token is displayed.

    4. In the route, invalidate the token by changing the value of the audience or issuer, and then access the route again.

      The value of the token, and the reasons that the token is invalid, are displayed.