Azure AD B2C SSO Integration
Overview
APEX uses Azure AD B2C as its identity provider for customer authentication. The system implements OAuth 2.0 and OpenID Connect protocols for secure authentication. This document provides detailed guidance on how to implement SSO with Azure AD B2C in your application.
Important: All Azure AD B2C configuration, tenant setup, and policy management is handled by the APEX team. Contact the APEX team at platform-dev@lightstone.co.za to request Azure configuration changes or to obtain your application's specific credentials.
Configuration Parameters
The Azure AD B2C configuration requires several key parameters that must be defined in your application's configuration. These parameters will be provided by the APEX team.
Parameter Descriptions
-
authority: The Azure AD B2C policy endpoint for sign-up/sign-in. This URL contains the Azure B2C tenant name and the user flow or custom policy to use for signing in.
- Standard value:
"https://login.lightstone.co.za/tfp/lsgb2c.onmicrosoft.com/B2C_1A_SIGNUPORSIGNINV2_TEST/"
- Standard value:
-
passwordReset: The password reset policy endpoint. This URL points to a separate user flow configured for password reset operations.
- Standard value:
"https://login.lightstone.co.za/tfp/lsgb2c.onmicrosoft.com/B2C_1A_PASSWORDRESETV2_TEST/"
- Standard value:
-
clientId: The application client ID registered in Azure AD B2C. This identifier is provided by the APEX team after they register your application in the Azure portal.
-
redirectUrl: The application redirect URL after successful authentication. Provide this URL to the APEX team to ensure it matches exactly what's registered in the Azure portal for your application.
-
logoutRedirectUrl: The URL to redirect to after logout. Provide this URL to the APEX team so they can configure Azure AD B2C to redirect users to this URL when they sign out.
-
scopes: The API permissions required by the application:
- Standard values:
["openid", "profile"] - If your application needs access to specific APIs, discuss additional scopes with the APEX team
- Standard values:
-
knownAuthorities: The list of trusted authority domains for authentication. Only these domains will be considered valid token issuers.
- Standard values:
["lsgb2c.b2clogin.com", "login.lightstone.co.za"]
- Standard values:
Implementation Guide
1. Application Registration
The APEX team will handle all Azure AD B2C tenant creation and application registration processes. To get your application registered:
- Contact the APEX team with the following information:
- Your application name and description
- Application type ("Web app / API" or "Single-page application")
- Redirect URLs for authentication
- Required API permissions and scopes
- Any custom user attributes needed
The APEX team will provide you with the necessary clientId and other configuration parameters after registration.
2. User Flow Configuration
The APEX team manages all user flow configurations in Azure AD B2C, including:
- Sign-up and sign-in: Combined flow that allows users to create new accounts or sign in
- Password reset: Flow that handles the password recovery process
- Profile editing: Optional flow that allows users to modify their profile information
If you need customizations to these flows, contact the APEX team with your specific requirements.
3. Client-Side Implementation
Implement the MSAL.js library in your application using the configuration provided by the APEX team:
import { PublicClientApplication } from "@azure/msal-browser";
const msalConfig = {
auth: {
clientId: "your-client-id", // Use the client ID provided by the APEX team
authority: "https://login.lightstone.co.za/tfp/lsgb2c.onmicrosoft.com/B2C_1A_SIGNUPORSIGNINV2_TEST/",
knownAuthorities: ["lsgb2c.b2clogin.com", "login.lightstone.co.za"],
redirectUri: "https://your-app.com/auth-redirect", // Your application's redirect URL
postLogoutRedirectUri: "https://your-app.com/logout-redirect" // Your application's logout redirect URL
},
cache: {
cacheLocation: "sessionStorage"
}
};
const msalInstance = new PublicClientApplication(msalConfig);
4. Authentication Flows
Sign-in
const loginRequest = {
scopes: ["openid", "profile"]
};
msalInstance.loginRedirect(loginRequest)
.catch(error => {
// Handle login errors
if (error.errorMessage.includes("AADB2C90118")) {
// Password reset error code - redirect to reset flow
msalInstance.loginRedirect({
authority: "https://login.lightstone.co.za/tfp/lsgb2c.onmicrosoft.com/B2C_1A_PASSWORDRESETV2_TEST/"
});
}
});
Token Acquisition
msalInstance.acquireTokenSilent(loginRequest)
.then(response => {
// Use the access token in the response to call your API
const accessToken = response.accessToken;
})
.catch(error => {
// Handle token acquisition errors
if (error instanceof InteractionRequiredAuthError) {
msalInstance.acquireTokenRedirect(loginRequest);
}
});
Sign-out
msalInstance.logout({
postLogoutRedirectUri: msalConfig.auth.postLogoutRedirectUri
});
Security Best Practices
- Token Validation: Always validate tokens on your server before granting access
- Secure Storage: Store tokens securely and never in local storage
- HTTPS Only: Ensure all communication happens over HTTPS
- State Parameter: Use state parameters to prevent CSRF attacks
- Scopes: Request only the scopes your application needs
Troubleshooting
Common issues and solutions:
| Issue | Solution |
|---|---|
| CORS errors | Contact the APEX team to ensure your app's domain is registered in the Azure portal |
| Invalid redirect URI | Ensure redirect URIs match what you provided to the APEX team |
| Token expired | Implement proper token refresh logic |
| Silent token acquisition fails | Fall back to interactive authentication |
| Configuration changes needed | Contact the APEX team for any Azure AD B2C configuration changes |
Federation Integration
Apex SSO supports two federation patterns:
- Your application as a client — your application accepts Apex B2C-issued JWTs. Users authenticate through the Apex sign-in page and your server validates tokens issued by B2C.
- Your identity provider as a federated IdP — your organisation's IdentityServer (or any OIDC-compatible IdP) is registered with Apex B2C. Users click a "Sign in with [Your Organisation]" button on the Apex login screen and authenticate through your IdP; Apex B2C trusts the resulting token.
Both patterns require contacting the APEX team to register your application. The sections below cover each case.
How Federation Works
When a user accesses a 3rd party application, they are redirected to Apex B2C for authentication. B2C either handles the login directly (local account or social IdP) or delegates to a registered federated identity provider. After authentication, B2C issues a JWT back to the 3rd party application. If the user is new to Apex, B2C automatically creates a contact record via the Apex SSO API before issuing the token.
Registering as a Federation Partner
Contact the APEX team at platform-dev@lightstone.co.za with the following information:
| Information | Notes |
|---|---|
| Application / organisation name | Used for display and internal tracking |
| Integration type | "Client application" (your app uses Apex B2C as IdP) or "Identity provider" (your IdP is added to Apex B2C) |
| OIDC Discovery URL | Required for IdP integration — your /.well-known/openid-configuration endpoint |
| Client ID | For IdP integration: the client ID your IdP assigns to Apex B2C |
| Redirect URIs | The URL(s) Apex B2C redirects back to after authentication |
| Required scopes | Any scopes beyond openid profile email |
| Display name for sign-in button | How your option appears on the Apex sign-in page |
The APEX team will provide:
- A
clientIdand authority URL for your application (client registration) - A
domain_hintvalue for IdP hint routing (see Forcing a Specific Identity Provider)
Server-Side Integration
If your application is server-side rather than a browser-only SPA, use your platform's OAuth2/OIDC middleware to handle the authentication flow.
Note on hostnames and URL format: The authority URL uses
login.lightstone.co.za(the Apex custom domain), while the redirect URI registered with the APEX team useslsgb2c.b2clogin.com(the native Azure AD B2C domain). Both hostnames resolve the same B2C tenant. Your OIDC middleware must trust both — include both in yourknownAuthorities(MSAL) or equivalent allowed-issuers configuration.Note on
/v2.0/: The authority URL for server-side OIDC middleware includes the/v2.0/suffix (e.g....B2C_1A_SIGNUPORSIGNINV2/v2.0/). This is required so the middleware can auto-discover the OIDC configuration at/.well-known/openid-configuration. The MSAL.js authority shown in the Configuration Parameters section above omits/v2.0/— this is correct for that library but will not work for server-side middleware. Use the/v2.0/form for all server-side frameworks.
- C#
- Java
- Python
// Program.cs
// using Microsoft.AspNetCore.Authentication.OpenIdConnect;
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddOpenIdConnect(options =>
{
options.Authority = "https://login.lightstone.co.za/tfp/lsgb2c.onmicrosoft.com/B2C_1A_SIGNUPORSIGNINV2/v2.0/";
options.ClientId = "your-client-id"; // Provided by APEX team
options.ClientSecret = "your-client-secret";
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.MetadataAddress =
"https://login.lightstone.co.za/tfp/lsgb2c.onmicrosoft.com/B2C_1A_SIGNUPORSIGNINV2/v2.0/.well-known/openid-configuration";
});
Replace
B2C_1A_SIGNUPORSIGNINV2with the policy name provided by the APEX team.
# application.yml
spring:
security:
oauth2:
client:
registration:
apex-b2c:
client-id: your-client-id # Provided by APEX team
client-secret: your-client-secret
scope: openid, profile, email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/apex-b2c" # {baseUrl} is a Spring Security runtime variable — do not replace it
provider:
apex-b2c:
issuer-uri: https://login.lightstone.co.za/tfp/lsgb2c.onmicrosoft.com/B2C_1A_SIGNUPORSIGNINV2/v2.0/
The example below uses Flask with Authlib. For FastAPI, see the Authlib FastAPI integration guide — the
register()call and token exchange are identical; only the route decorators differ.
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
apex_b2c = oauth.register(
name='apex_b2c',
client_id='your-client-id', # Provided by APEX team
client_secret='your-client-secret',
server_metadata_url=(
'https://login.lightstone.co.za/tfp/lsgb2c.onmicrosoft.com'
'/B2C_1A_SIGNUPORSIGNINV2/v2.0/.well-known/openid-configuration'
),
client_kwargs={'scope': 'openid profile email'},
)
@app.route('/login')
def login():
redirect_uri = url_for('auth_callback', _external=True)
return apex_b2c.authorize_redirect(redirect_uri)
@app.route('/auth/callback')
def auth_callback():
token = apex_b2c.authorize_access_token()
user_info = token.get('userinfo')
return user_info
Forcing a Specific Identity Provider
If your application uses a federated IdP registered with Apex B2C, you can bypass the Apex sign-in screen entirely and send users directly to your identity provider using the domain_hint parameter. The APEX team provides the correct domain_hint value for your provider at registration time.
The domain_hint value matches the domain configured in the Apex B2C policy for your provider. Using an incorrect value causes the normal sign-in screen to display rather than routing to your IdP.
- MSAL.js
- C#
- Java
- Python
const loginRequest = {
scopes: ["openid", "profile"],
extraQueryParameters: {
domain_hint: "your-provider-domain" // Dot-separated domain identifier — value provided by APEX team at registration
}
};
msalInstance.loginRedirect(loginRequest);
// or
await msalInstance.loginPopup(loginRequest);
// using Microsoft.AspNetCore.Authentication.OpenIdConnect;
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
// Value provided by APEX team at registration time
context.ProtocolMessage.SetParameter("domain_hint", "your-provider-domain");
return Task.CompletedTask;
}
};
// import org.springframework.security.config.annotation.web.builders.HttpSecurity;
// import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
// import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
// import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
// import org.springframework.security.web.SecurityFilterChain;
// import java.util.Map;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
ClientRegistrationRepository repo) throws Exception {
var baseResolver = new DefaultOAuth2AuthorizationRequestResolver(
repo, "/oauth2/authorization");
http.oauth2Login(login -> login
.authorizationEndpoint(ep -> ep
.authorizationRequestResolver(request -> {
var authRequest = baseResolver.resolve(request);
if (authRequest == null) return null;
return OAuth2AuthorizationRequest.from(authRequest)
.additionalParameters(Map.of(
"domain_hint", "your-provider-domain" // APEX team value
))
.build();
})
)
);
return http.build();
}
}
The example below uses Flask with Authlib. For FastAPI, pass
domain_hintas an extra parameter in the same way — see the Authlib FastAPI integration guide.
@app.route('/login')
def login():
redirect_uri = url_for('auth_callback', _external=True)
return apex_b2c.authorize_redirect(
redirect_uri,
domain_hint='your-provider-domain' # Value provided by APEX team
)
Account Resolution Scenarios
When a user authenticates via a federated identity provider, Apex B2C resolves the user's identity using their email address. The following scenarios describe what happens depending on whether the user already exists in each system.
Scenario A — New to both Apex and the 3rd party system
The user has never accessed either system. They authenticate via the federated IdP for the first time.
Scenario B — New to Apex, existing in 3rd party system
The user already has an account with the partner's system but has never signed into Apex. This is the most common first-login pattern for federation rollouts.
Note: From B2C's perspective, Scenarios A and B are technically identical — B2C has no visibility into whether the user already has an account with the partner IdP. The B2C flow is the same in both cases; the scenarios differ only at the user-experience level (new vs. returning in the partner system).
Note: Email is the identity correlation key. B2C always creates a new B2C user account for first-time federated logins (the
alternativeSecurityIdnot found → write new). Email-based deduplication of Apex contacts happens inside the/Identity/OnboardUserAPI call (Step 14), not in B2C itself. If the user's email matches an existing Apex contact,OnboardUserlinks the federated identity to that contact rather than creating a duplicate contact.
On API errors: If
/Identity/LookupIdentityor/Identity/OnboardUserreturns an error, B2C will abort the authentication flow and return an error response to the 3rd party application. The user will see a B2C error page and will not receive a JWT. Your application should handle OAuth2 error responses on the redirect callback accordingly.
Scenario C — Existing in both Apex and 3rd party system
The user has previously signed in to Apex via this federated provider. The B2C account already has the alternativeSecurityId for this IdP. No onboarding call is made.
Reference
- MSAL.js Documentation
- OpenID Connect Specifications
- Contact APEX Support for Azure AD B2C configuration assistance