Skip to content

Custom OIDC Integration (Deployment Plan)


Introduction

Guance Deployment Plan supports integration with third-party Identity Providers (IdPs) via custom OIDC to handle the following scenarios:

  • A single IdP whose OIDC flow or response structure differs from the standard implementation.
  • Multiple IdPs coexisting, requiring differentiation at the login entry point based on source.
  • The need to take over address generation, account information transformation, or account normalization during the code -> token -> userinfo process.

This document is based on the "OIDC Configuration Instructions for Multiple IDPs" and supplements the usage for single IDP scenarios. Both scenarios share the same set of custom OIDC capabilities, with the main differences lying in the frontend login entry and Func routing logic.

Prerequisites

  • Guance base version is 1.123.216 or higher.
  • Possess configuration permissions for the Deployment Plan Launcher.
  • The built-in Func is enabled, and function APIs can be created.
  • Have obtained information such as client_id, client_secret, authorization address, token address, and userinfo address from the target IdP.
  • Have clarified the mapping relationship between user information fields and Guance account fields.

Implementation Principle

The core of the custom OIDC solution is to delegate key OIDC processes to Func:

  1. OIDCClientSet.wellKnowURL no longer points directly to the IdP's .well-known/openid-configuration, but to the well_know function in Func.
  2. During login, Guance internally calls Func's get_auth_url via authorization_endpoint, which returns the final login address.
  3. During callback, Guance internally calls Func's get_userinfo via userinfo_endpoint. This function internally completes code -> token -> userinfo.
  4. After normalizing user information from different IdPs, Func returns it directly to Guance, which then completes account matching and login.

Differences Between Single IDP and Multiple IDPs

Scenario Frontend Login Entry Func Routing Method Callback Address
Single IDP Only one login button When type is not passed, directly uses the default IdP May not include type, or can include a fixed value
Multiple IDPs Multiple login buttons, each carrying a different type main_oidc dispatches to different IdP sub-scripts based on type Must match the callback address of the corresponding IdP, typically includes type=xxx

For a single IDP, it is recommended to hardcode the default value directly in the main script, for example: xtype = args.get("type") or "keycloak". This way, if expansion to multiple IDPs is needed later, only the frontend entry and dispatch logic need to be supplemented.

Steps

1. Configure OIDCClientSet in forethought-core/core

In Launcher, navigate to Namespace: forethought-core > core, and add or modify the following configuration in config.yaml:

OIDCClientSet:
  # Enable custom OIDC
  enableCustomOIDC: true

  # Points to the API address of the well_know function in Func
  wellKnowURL: "<Func well_know API address>"

  mapping:
    username: preferred_username
    mobile: mobile
    email: email
    exterId: sub

Explanation:

  • enableCustomOIDC: true is the key switch to enable custom OIDC.
  • wellKnowURL must point to the well_know function in Func, not directly to the IdP's service discovery address.
  • The field names in mapping need to be consistent with the final user information structure returned by Func.

2. Configure Frontend Login Entry

In Launcher, navigate to Namespace: forethought-webclient > front-web-config, and modify config.js.

Single IDP Example

window.DEPLOYCONFIG = {
  ...
  paasCustomLoginInfo: [
    {
      label: "OIDC Login",
      url: "https://<Deployment Plan Web Domain>/oidc/login",
      desc: "Custom OIDC Login"
    }
  ],
  paasCustomLoginUrl: "https://<IdP Logout Address>?redirect_url=https://<Deployment Plan Web Domain>/oidc/login"
}

Multiple IDPs Example

window.DEPLOYCONFIG = {
  ...
  paasCustomLoginInfo: [
    {
      iconUrl: "https://<Icon URL>",
      label: "Keycloak Login",
      url: "https://<Deployment Plan Web Domain>/oidc/login?type=keycloak",
      desc: "OIDC Keycloak Login"
    },
    {
      iconUrl: "https://<Icon URL>",
      label: "Authing Login",
      url: "https://<Deployment Plan Web Domain>/oidc/login?type=authing",
      desc: "OIDC Authing Login"
    }
  ]
}

Explanation:

  • In multiple IDP scenarios, it is recommended to identify the source using the type parameter.
  • This type will be passed through to Func for the main script to distinguish between different IdPs.
  • If the IdP callback address also depends on type, the redirect_uri in the frontend login entry, IdP configuration, and Func sub-scripts must all be consistent.

Supplement: Configuring SSO Logout Address

If you want users to log out from the third-party authentication center simultaneously when logging out from Guance, you also need to add paasCustomLoginUrl in config.js under front-web-config:

window.DEPLOYCONFIG = {
  ...
  paasCustomLoginInfo: [
    {
      label: "OIDC Login",
      url: "https://<Deployment Plan Web Domain>/oidc/login",
      desc: "Custom OIDC Login"
    }
  ],
  paasCustomLoginUrl: "https://<IdP end_session_endpoint>?redirect_url=https://<Deployment Plan Web Domain>/oidc/login"
}

Explanation:

  • paasCustomLoginUrl is the third-party logout address, typically referencing the end_session_endpoint in the IdP's .well-known/openid-configuration.
  • In the custom OIDC solution, OIDCClientSet.wellKnowURL points to Func's well_know interface, but paasCustomLoginUrl should still be filled with the final third-party IdP's logout address, not the Func API address.
  • In the original PDF configuration example, the third-party logout address commonly uses the redirect_url parameter to redirect back to https://<Deployment Plan Web Domain>/oidc/login.
  • If your IdP uses post_logout_redirect_uri, returnTo, or other parameter names, follow the actual protocol requirements of that IdP.
  • paasCustomLoginUrl has only one configuration value, so in multiple IDP scenarios, an additional unified logout strategy needs to be designed, such as having an intermediate logout address that redirects to the corresponding IdP based on context.
  • If you want to directly trigger third-party authentication when accessing the site without being logged in, you can also configure paasCustomLoginUrl directly as /oidc/login.

3. Configure Web Nginx Forwarding Rules

Modify nginx.conf in Namespace: forethought-webclient > front-web-config to forward OIDC login and callback requests to the inner service:

location /oidc/login {
    proxy_connect_timeout 5;
    proxy_send_timeout 5;
    proxy_read_timeout 300;
    proxy_http_version 1.1;
    proxy_set_header Connection "keep-alive";
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Headers X-Requested-With;
    add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
    proxy_pass http://inner.forethought-core:5000/api/v1/inner/oidc/login;
}

location /oidc/callback {
    proxy_connect_timeout 5;
    proxy_send_timeout 5;
    proxy_read_timeout 300;
    proxy_http_version 1.1;
    proxy_set_header Connection "keep-alive";
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Headers X-Requested-With;
    add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
    proxy_pass http://inner.forethought-core:5000/api/v1/inner/oidc/callback;
}

The purpose of this step is to ensure that browser requests to /oidc/login and /oidc/callback ultimately reach the internal OIDC processing logic of the Deployment Plan.

4. Write the Main Script in Func

The main script is responsible for providing a unified external entry point and dispatching to different IdP sub-scripts based on type. It is recommended to include at least the following three functions:

  • well_know: Returns service discovery information used by OIDCClientSet.wellKnowURL.
  • get_auth_url: Generates and returns the final authentication address for redirecting to the IdP.
  • get_userinfo: Receives callback parameters and internally completes code -> token -> userinfo.

Example:

import __keycloak as keycloak_client
import __authing as authing_client

@DFF.API('OIDC Service Discovery Interface')
def well_know():
    return {
        "authorization_endpoint": "<Func get_auth_url API address>",
        "token_endpoint": "",
        "userinfo_endpoint": "<Func get_userinfo API address>"
    }

@DFF.API('Get Login Address Information')
def get_auth_url(**kwargs):
    args = kwargs.get("args", {})
    xtype = args.get("type") or "keycloak"

    if xtype == "keycloak":
        return keycloak_client.get_auth_url(**kwargs)
    elif xtype == "authing":
        return authing_client.get_auth_url(**kwargs)
    else:
        raise Exception(f"Illegal value type=`{xtype}`")

@DFF.API('User Information Retrieval Interface')
def get_userinfo(**kwargs):
    args = kwargs.get("args", {})
    xtype = args.get("type") or "keycloak"

    if xtype == "keycloak":
        return keycloak_client.get_userinfo(**kwargs)
    elif xtype == "authing":
        return authing_client.get_userinfo(**kwargs)
    else:
        raise Exception(f"Illegal value type=`{xtype}`")

Explanation:

  • The authorization_endpoint and userinfo_endpoint returned by well_know are actually Func API addresses.
  • token_endpoint can usually be left empty in this solution because the code -> token process is already completed internally within get_userinfo.
  • It is recommended to retain this main script layer even for single IDP scenarios for easier future expansion.

5. Write Sub-scripts for Each IdP

It is recommended to maintain each IdP sub-script independently, such as __keycloak.py, __authing.py. The sub-script should at least implement the following:

  • OIDC_SET: Client configuration for the current IdP.
  • well_know: The real OIDC endpoints for the current IdP.
  • get_auth_url: Generates the authentication address.
  • turn_token: Exchanges the callback code for a Token.
  • turn_userinfo: Retrieves user information based on the Token.
  • get_userinfo: Unified external entry point, internally calls turn_token and turn_userinfo sequentially.

Example structure:

OIDC_SET = {
    "client_id": "<OIDC Client ID>",
    "client_secret": "<OIDC Client Secret>",
    "scope": "openid profile email address",
    "redirect_uri": "https://<Deployment Plan Web Domain>/oidc/callback?type=authing",
    "grant_type": "authorization_code"
}

def well_know():
    return {
        "authorization_endpoint": "https://<IdP Authorization Address>",
        "token_endpoint": "https://<IdP Token Address>",
        "userinfo_endpoint": "https://<IdP Userinfo Address>",
        "end_session_endpoint": "https://<IdP Logout Address>",
        "jwks_uri": "https://<IdP JWKS Address>"
    }

!!! warning

The above `OIDC_SET` is only used to illustrate the configuration structure. It is not recommended to hardcode `client_secret`, tokens, or other sensitive credentials directly in Func scripts.

If managing secret information is necessary, prioritize injection and reading through password-type environment variables on the Func side. If `OIDCClientSet.clientSecret` still needs to be used in specific integration scenarios, it is also recommended to manage it securely in the same way, avoiding plaintext writing in scripts or configuration examples.

Key considerations:

  • In multiple IDP scenarios, redirect_uri must include the corresponding type=xxx parameter.
  • The callback address registered in the IdP backend must exactly match OIDC_SET.redirect_uri.
  • The return value of get_auth_url must be fixed as {"url": "..."}.
  • The return value of get_userinfo must be account information JSON, and it is recommended to place user attributes directly in the first-level structure.

SSO Logout Explanation

According to the deployment instructions in the PDF "001-Deployment Plan [OIDC] Third-party Authentication Login Configuration", the SSO logout methods currently supported by Guance are as follows:

  1. The user clicks logout on the Guance side.
  2. The system first logs out the local session.
  3. After the local session logout is complete, the browser redirects to the third-party logout address specified by paasCustomLoginUrl.
  4. After the third-party authentication center completes the logout, it redirects back to the login page or a specified page according to its parameter rules.

Limitations:

  • The current system cannot yet handle logout requests "initiated actively by the third-party authentication center".
  • In other words, it only supports logout flows triggered by Guance, not the scenario where the IdP initiates single logout and then calls back Guance to complete coordinated logout.
  • Therefore, during the integration phase, it is recommended to confirm with the customer in advance the two questions: "who is responsible for initiating logout" and "where to redirect after logout".

6. Unify User Information Fields

The user information finally returned by Func to Guance needs to correspond one-to-one with OIDCClientSet.mapping. It is recommended to ensure at least the following fields exist:

Guance Account Field IdP Field Example
username preferred_username
email email
mobile mobile
exterId sub

If the same person uses the same email across multiple IdPs, it is recommended to uniformly handle it as the same account identifier in Func, for example:

  • Set email uniformly to the corporate email.
  • Set sub or exterId uniformly to the email value.
  • When different IdPs return inconsistent structures, first convert them into a unified JSON within Func before returning.

This step is key to avoiding duplicate account generation when logging in with multiple IdPs.

7. Clean Up Historical OIDC Accounts if Necessary

If old OIDC accounts already exist in the environment, and different IdPs have caused multiple accounts to be generated for the same email, they can be handled as follows:

-- Query if there are duplicate accounts with the same email among OIDC accounts
select email, count(id) as num
from `main_account`
where status = 0 and `exterId` <> ""
group by email
having num > 1;

-- After cleaning up redundant accounts, you can uniformly update exterId to email
update `main_account`
set exterId = email
where status = 0 and `exterId` <> "";

Please back up before execution and handle with caution based on the actual account status.

Debugging Suggestions

  • During initial integration, you can print account information at the end of get_userinfo and temporarily throw an exception to first confirm if the fields meet expectations before enabling login.
  • If duplicate accounts appear, first check whether email, sub, and exterId returned by different IdPs have been unified.
  • If login fails after callback, first check whether redirect_uri exactly matches the value registered in the IdP backend.
  • When converting from a single IDP to multiple IDPs, first retain the default type, then gradually add new login entries and sub-scripts.

FAQs

1. Why can't wellKnowURL directly point to the IdP's .well-known address?

Because the goal of this solution is not only to read standard OIDC endpoints but also to delegate login address generation, callback handling, and account normalization logic to Func. Therefore, wellKnowURL needs to point to the custom well_know function.

Because many single IDP scenarios are essentially "non-standard OIDC" integrations, such as needing to modify login addresses, callback parameters, user information structures, or account primary keys. Using this approach can uniformly handle these compatibility issues.

3. What if the user information structure returned by the IdP is deeply nested?

It is recommended to first parse and flatten it in Func, then organize the final result into a first-level structure before returning it to Guance. Do not pass complex nested structures directly to the system account mapping.

Feedback

Is this page helpful? ×