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 -> userinfoprocess.
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.216or 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:
OIDCClientSet.wellKnowURLno longer points directly to the IdP's.well-known/openid-configuration, but to thewell_knowfunction in Func.- During login, Guance internally calls Func's
get_auth_urlviaauthorization_endpoint, which returns the final login address. - During callback, Guance internally calls Func's
get_userinfoviauserinfo_endpoint. This function internally completescode -> token -> userinfo. - 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: trueis the key switch to enable custom OIDC.wellKnowURLmust point to thewell_knowfunction in Func, not directly to the IdP's service discovery address.- The field names in
mappingneed 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
typeparameter. - This
typewill be passed through to Func for the main script to distinguish between different IdPs. - If the IdP callback address also depends on
type, theredirect_uriin 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:
paasCustomLoginUrlis the third-party logout address, typically referencing theend_session_endpointin the IdP's.well-known/openid-configuration.- In the custom OIDC solution,
OIDCClientSet.wellKnowURLpoints to Func'swell_knowinterface, butpaasCustomLoginUrlshould 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_urlparameter to redirect back tohttps://<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. paasCustomLoginUrlhas 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
paasCustomLoginUrldirectly 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 byOIDCClientSet.wellKnowURL.get_auth_url: Generates and returns the final authentication address for redirecting to the IdP.get_userinfo: Receives callback parameters and internally completescode -> 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_endpointanduserinfo_endpointreturned bywell_knoware actually Func API addresses. token_endpointcan usually be left empty in this solution because thecode -> tokenprocess is already completed internally withinget_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 callbackcodefor a Token.turn_userinfo: Retrieves user information based on the Token.get_userinfo: Unified external entry point, internally callsturn_tokenandturn_userinfosequentially.
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_urimust include the correspondingtype=xxxparameter. - The callback address registered in the IdP backend must exactly match
OIDC_SET.redirect_uri. - The return value of
get_auth_urlmust be fixed as{"url": "..."}. - The return value of
get_userinfomust 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:
- The user clicks logout on the Guance side.
- The system first logs out the local session.
- After the local session logout is complete, the browser redirects to the third-party logout address specified by
paasCustomLoginUrl. - 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
emailuniformly to the corporate email. - Set
suborexterIduniformly 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_userinfoand temporarily throw an exception to first confirm if the fields meet expectations before enabling login. - If duplicate accounts appear, first check whether
email,sub, andexterIdreturned by different IdPs have been unified. - If login fails after callback, first check whether
redirect_uriexactly 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.
2. Why is this solution also recommended for single IDP scenarios?¶
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.