This documentation assumes that you have completed through step 9 from the Installing mojoPortal documentation.
Due to an older bug, you will need mojoPortal v4.0.0 to make this work with providers other than Keycloak.
OpenID Connect (OIDC) Authentication in mojoPortal
mojoPortal includes built-in support for external authentication and role synchronization using OpenID Connect (OIDC) / OAuth 2.0 (Authorization Code Flow). When enabled, OIDC replaces the standard local Forms authentication workflow, redirecting login, registration, password recovery, and logout actions to your external Identity Provider (IdP) such as Keycloak, Auth0, Entra ID, or Duende IdentityServer.
1. Architecture Overview
The OIDC integration consists of five main components:
AppConfig.OAuthConfiguration : Reads the OAuth:* configuration settings from user.config or Web.config and exposes AppConfig.OAuth.Configured .
OidcService : Initialized at application startup in Global.Application_Start . Manages OIDC discovery metadata ( DiscoveryCache ), JSON Web Key caching ( JsonWebKeyCache ), authorization URL generation, token exchange, token refresh, JWT validation, and PageUrlService link overrides.
ExternalLoginController : An MVC controller registered by RouteRegistrar.RegisterRoutes that handles the OIDC authorization callback ( /ExternalLogin/Callback ), single sign-out ( /ExternalLogin/Logout ), and error display ( /ExternalLogin/Error ).
AuthHandlerHttpModule : Hooks into the ASP.NET pipeline asynchronously ( AuthenticateRequestAsync ) to validate token cookies on each request, auto-provision local SiteUser accounts on first login, and set HttpContext.Current.User to a mojoPrincipal backed by mojoIdentity .
mojoRoleProvider : Synchronizes user roles from the OIDC Access Token's resource_access claim with local mojoPortal site roles.
2. Identity Provider (IdP) Requirements
Configure your OIDC Identity Provider client with the following settings:
| Setting |
Required Value / Behavior |
| Discovery Endpoint |
Must serve /.well-known/openid-configuration under OAuth:Authority with authorization_endpoint , token_endpoint , jwks_uri , end_session_endpoint , and optionally registration_endpoint . |
| Client Authentication |
Confidential client (Client ID + Client Secret). |
| Grant Types |
Authorization Code ( response_type=code ) and Refresh Token. |
| Scopes |
openid (requested by OidcService.GetLoginUrlAsync ), plus profile/email claims included in the Identity Token. |
| Valid Redirect URI |
https://<your-mojo-domain>/ExternalLogin/Callback |
| Valid Post-Logout Redirect URI |
https://<your-mojo-domain>/ (or your site root URL from SiteUtils.GetNavigationSiteRoot() ) |
Access Token Audience ( aud ) |
Must match OAuth:ClientId (validated in OidcService.ValidateTokenAsync ). |
Access Token Issuer ( iss ) |
Must match OAuth:Authority . |
Some documentation to providers that you might be using:
WARNING
Access Token Validation: OidcService.ValidateTokenAsync validates the Access Token (not just the ID Token) as a JWT signed by the IdP's JWKS keys, requiring aud to match OAuth:ClientId and iss to match OAuth:Authority .
- Ensure your IdP issues JWT access tokens (not opaque tokens).
- In Keycloak, add an Audience Protocol Mapper to your client so that
OAuth:ClientId is included in the aud claim of the Access Token.
3. Configuration ( user.config )
To enable OIDC, add the following three keys to your <appSettings> in user.config:
<!-- Base URL of your OIDC Identity Provider / Realm -->
<add key="OAuth:Authority" value="https://idp.example.com/realms/my-realm" />
<!-- Client ID registered in your Identity Provider -->
<add key="OAuth:ClientId" value="my-client-id" />
<!-- Client Secret generated by your Identity Provider -->
<add key="OAuth:ClientSecret" value="your-client-secret" />
Optional Redirect Configuration
By default, after a successful login callback without a specific returnurl , mojoPortal redirects to the site root ( ~/ ). You can customize this destination in user.config :
<add key="LoginRedirectLink" value="~/my-dashboard" />
4. Required Token Claims & User Auto-Provisioning
On every authenticated request, AuthHandlerHttpModule.AuthenticateRequestAsync reads claims from the Identity Token ( ExternalLogin:IdentityToken cookie) to identify or create the mojoPortal user:
| Claim Type |
Token |
Requirement |
Mapped SiteUser Property |
email |
Identity Token |
Required |
Email and LoginName (lowercased). Used to check if the user exists via SiteUser.EmailExistsInDB . |
name |
Identity Token |
Required for new users |
Name (Display Name). If missing when creating a new user, provisioning aborts. |
given_name |
Identity Token |
Optional |
FirstName (defaults to "" ). |
family_name |
Identity Token |
Optional |
LastName (defaults to "" ). |
First-Time Login Provisioning
When a user authenticates via OIDC and their email does not yet exist in the current site's database:
- A new
SiteUser is created with LoginName and Email set to the email claim, Name set to name , and a randomly generated encoded password.
- Existing newsletter subscriptions matching the email are claimed via
NewsletterHelper.ClaimExistingSubscriptions .
- Registered
UserRegisteredHandlerProvider events are fired.
5. Role Synchronization
When AppConfig.OAuth.Configured is enabled, mojoRoleProvider.GetRolesAndSetCookieInternal bypasses standard role cookies and instead synchronizes roles from the Access Token's resource_access claim (the standard Keycloak client-role claim format):
{
"resource_access": {
"easy-image": {
"roles": [
"Admins",
"Content Publishers"
]
}
}
}
How Role Sync Works
mojoRoleProvider reads the resource_access claim from Global.OidcService.AccessToken and deserializes it into mojoRoleProvider.ResourceAccess .
- Adding Roles: Any role listed in the token that the user does not currently have in mojoPortal—provided a role with that exact
RoleName already exists in the mojoPortal site—is granted via Role.AddUser .
- Removing Roles: Any mojoPortal role currently assigned to the user that is not present in the token's role array is removed from the user via
Role.RemoveUser .
6. Authentication & Session Lifecycle
- Link Overrides & Page Redirects:
- Cookies & Automatic Token Refresh:
OidcService.SetTokenCookies stores three persistent cookies: ExternalLogin:AccessToken , ExternalLogin:IdentityToken , and ExternalLogin:RefreshToken .
- On each request,
OidcService.IsAuthenticated validates ExternalLogin:AccessToken . If SecurityTokenExpiredException is thrown, it automatically calls GetTokensByRefreshToken using ExternalLogin:RefreshToken and updates all three cookies.
- Single Sign-Out:
- Clicking Log Out routes to
ExternalLoginController.Logout , which calls OidcService.Logout to expire the three ExternalLogin:* cookies and redirect the browser to the IdP's end_session_endpoint with id_token_hint and post_logout_redirect_uri .
7. Troubleshooting
- Redirected to
/ExternalLogin/Error with an Incident Number:
- Check the mojoPortal system log (
Data/currentlog.config or System Log in Admin) for (Incident <Guid>) . ExternalLoginController.Callback logs the exact validation failure message.
- The most common cause is an
aud (Audience) mismatch in the Access Token—verify that your IdP includes OAuth:ClientId in the Access Token's aud claim.
- User logs in at IdP, returns to mojoPortal, but remains unauthenticated:
- Check the mojoPortal log for messages from
AuthHandlerHttpModule :
"User tried to authenticate but no email claim was found in the identity token." -> Ensure your IdP includes the email claim in the Identity Token even when only the openid scope is requested.
"User tried to authenticate but no name claim was found in the identity token." -> Ensure the name (full name) claim is mapped into the Identity Token for new users.