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:


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: 


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:

  1. A new SiteUser is created with LoginName and Email set to the email claim, Name set to name , and a randomly generated encoded password.
  2. Existing newsletter subscriptions matching the email are claimed via NewsletterHelper.ClaimExistingSubscriptions .
  3. 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
  1. mojoRoleProvider reads the resource_access claim from Global.OidcService.AccessToken and deserializes it into mojoRoleProvider.ResourceAccess .
  2. 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 .
  3. 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

OIDC Identity Provider mojoPortal User Browser OIDC Identity Provider mojoPortal User Browser Click Sign In (or visit /Secure/Login.aspx) Redirect to IdP Authorize Endpoint (?client_id=...&response_type=code&scope=openid) Authenticate at IdP Redirect to /ExternalLogin/Callback?code=...&iss=... GET /ExternalLogin/Callback POST Authorization Code to Token Endpoint Access Token, Identity Token, Refresh Token Validate Access Token & Set Token Cookies Redirect to Site Root (or returnurl) Subsequent Request (with Token Cookies) AuthHandlerHttpModule validates Access Token (refreshes if expired) Auto-provision SiteUser (if new) & sync roles
  1. Link Overrides & Page Redirects:
  2. Cookies & Automatic Token Refresh:
  3. Single Sign-Out:

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.