Microsoft Login Automation¶
This document describes the technical implementation of Microsoft OAuth authentication for the Spokane Mountaineers Experience Cloud site, backed by the Microsoft Identity Platform (OpenID Connect / OAuth 2.0 v2.0).
Access¶
- Apex Class: MicrosoftAuthRegistrationHandler
- Test Class: MicrosoftAuthRegistrationHandlerTest
- Auth Provider: Setup → Identity → Auth. Providers → Microsoft
- Microsoft Entra App Registration: managed by Terraform/OpenTofu in
Spokane-Mountaineers/infrastructure- seeterraform/environments/{staging,production}/and the infrastructure plan.
Purpose¶
The MicrosoftAuthRegistrationHandler implements Salesforce's Auth.RegistrationHandler interface to manage user authentication for "Sign in with Microsoft" on Experience Cloud sites. It:
- Authenticates existing users via Microsoft OAuth.
- Matches users by username pattern (
email + '.smi'). - Falls back to the UPN (
data.username) when Microsoft returns an emptyemailclaim. - Prevents new account creation (Donorbox-only registration).
- Provides actionable error messages for users.
Configuration¶
Prerequisites¶
- My Domain is deployed.
- Microsoft Entra App Registration exists for the target environment, managed in the infrastructure repo. The first apply produces a
client_idandclient_secretused below. - Microsoft Auth Provider is configured in the Salesforce org with:
- Provider Type: Open ID Connect
- URL Suffix:
Microsoft - Consumer Key:
client_idfromtofu output -raw client_id - Consumer Secret:
client_secretfromtofu output -raw client_secret - Authorize Endpoint URL:
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?prompt=select_account- theprompt=select_accountquery param forces Microsoft to show its account picker on every sign-in attempt instead of silently auto-selecting the only/most-recent session. Without it, a user already signed in to a single Microsoft account is taken straight through with no chance to pick a different one. Salesforce preserves static query params on the authorize URL and appends the OAuth params with&, so the resulting request carries both the staticpromptand the standardclient_id,redirect_uri, etc. - Token Endpoint URL:
https://login.microsoftonline.com/common/oauth2/v2.0/token - User Info Endpoint URL:
https://graph.microsoft.com/oidc/userinfo - Default Scopes:
openid email profile - Registration Handler:
MicrosoftAuthRegistrationHandler
- Experience Cloud Site has:
- Microsoft Auth Provider enabled in Login & Registration settings
- Self-Registration disabled (to prevent bypassing Donorbox)
Setup Steps¶
-
Provision the Entra App Registration (infrastructure repo):
cd infrastructure/terraform/environments/staging tofu init tofu apply -var-file=terraform.tfvars # first pass with placeholder callback URL tofu output -raw client_id tofu output -raw client_secret # sensitiveSee
docs/bootstrap.mdfor the one-time GCS state-backend andaz loginsetup. -
Configure Microsoft Auth Provider (Salesforce):
- Navigate to: Setup → Identity → Auth. Providers → New
- Provider Type: Open ID Connect
- Use the values from step 1 plus the endpoint URLs listed under Prerequisites
- Assign Registration Handler:
MicrosoftAuthRegistrationHandler - Set "Execute Registration As" to a user with appropriate permissions
- Save. Salesforce generates the Callback URL.
-
Update Entra App with the real callback URL (infrastructure repo):
- Edit
infrastructure/terraform/environments/staging/terraform.tfvarsand setsalesforce_callback_urlto the value Salesforce just generated. - Re-run
tofu apply -var-file=terraform.tfvars. The redirect URI is updated in place.
- Edit
-
Enable for Experience Cloud Site:
- Navigate to: Setup → Digital Experiences → All Sites → Spokane Mountaineers → Administration → Login & Registration
- Under Authentication Providers, enable Microsoft
- Confirm Self-Registration is Disabled
-
Add to Login Page:
- The login page is
force-app/main/default/pages/CommunitiesLogin.page. The "Continue with Microsoft" button is wired to{!microsoftLoginUrl}and ships with the Apex deploy.
- The login page is
Branding the Builder login button¶
Experience Builder's standard Login component renders each enabled auth provider as <img src="{iconUrl}"> {friendlyName}. We ship the official "Sign in with Microsoft" lockup as a Static Resource and reference it via the AuthProvider's iconUrl (/resource/Microsoft_Logo). Because the lockup SVG already contains the "Sign in with Microsoft" text, friendlyName is left as plain Microsoft to avoid rendering the same words twice.
Updating the lockup SVG runs into a Cloudflare caching gotcha:
- Salesforce serves community static resources through Cloudflare with a long TTL (
Cache-Control: public, max-age=3888000— 45 days). - Updating the Static Resource updates the bytes at origin, but Cloudflare keeps serving the previous version at the bare URL until the TTL expires. End users see the old icon for weeks.
- Cloudflare keys cache by the full URL including query string, so any unique query string forces a fresh origin fetch and gets the new bytes.
The fix is a versioned query string on the iconUrl. Bump v=N whenever the SVG bytes change:
<iconUrl>/resource/Microsoft_Logo?v=2</iconUrl>
Workflow for any future SVG update:
- Edit
force-app/main/default/staticresources/Microsoft_Logo.svg. - Bump
?v=Ninforce-app/main/default/authproviders/Microsoft.authprovider-meta.xml. - Deploy:
sf project deploy start --metadata "StaticResource:Microsoft_Logo"thenmake deploy-microsoft-auth SF_ENV=<env>.
The same pattern applies to any other absolute-path static resource referenced from long-lived configuration (e.g. brand logos, fonts).
Implementation Details¶
User Matching Strategy¶
Identical to the Google handler - username pattern matching:
String resolvedEmail = String.isNotBlank(data.email) ? data.email : data.username;
String usernamePattern = resolvedEmail.toLowerCase().trim() + '.smi';
List<User> usersByUsername = [
SELECT Id, Username, Email, IsActive
FROM User
WHERE Username = :usernamePattern
LIMIT 1
];
Microsoft-Specific: UPN Fallback¶
Microsoft Identity Platform returns user data via the OIDC userinfo endpoint. For most accounts the email claim and the UPN match. For some Entra accounts the email claim is empty (the account has no verified email), but the UPN - which Salesforce surfaces as data.username - is populated and looks like an email address. The handler falls back to data.username when data.email is blank so these users can still sign in.
Account Creation Prevention¶
Same contract as the Google handler - no new accounts are created. If no matching user is found:
throw new RegistrationHandlerException(
'No account found. New members: sign up at donorbox.org/spokanemountaineers-membership-2. ' +
'Existing members: contact webdev@spokanemountaineers.org to link your Microsoft account.'
);
Flow Diagram¶
Source: microsoft-login-flow.d2
Render with d2 microsoft-login-flow.d2 microsoft-login-flow.svg.
Code Structure¶
Main Methods¶
createUser(Id portalId, Auth.UserData data)¶
Main entry point called by Salesforce during Microsoft OAuth flow.
Logic:
- Resolve
data.email; if blank, fall back todata.username(UPN). - If still blank, throw
RegistrationHandlerException('Email is required for authentication'). - Call
findExistingUser()to locate user by username pattern. - If found, return the user.
- If not found, throw exception with Donorbox / webdev guidance.
findExistingUser(String email)¶
Builds email.toLowerCase().trim() + '.smi' and queries the User table for a matching Username. Returns the first match or null.
updateUser(Id userId, Id portalId, Auth.UserData data)¶
No-op. Existing flows handle user updates.
Testing¶
Test Coverage¶
MicrosoftAuthRegistrationHandlerTest mirrors GoogleAuthRegistrationHandlerTest with one additional test:
testCreateUser_FallsBackToUpnWhenEmailBlank- verifies that when Microsoft returns an emptyemailclaim with a populated UPN, the handler resolves the user via the UPN.
Running Tests¶
sf apex run test --class-names MicrosoftAuthRegistrationHandlerTest --target-org staging --code-coverage
Error Handling¶
No Account Found¶
User sees:
No account found. New members: sign up at donorbox.org/spokanemountaineers-membership-2.
Existing members: contact webdev@spokanemountaineers.org to link your Microsoft account.
Blank Email¶
User sees:
Email is required for authentication
This is reached only when both data.email and data.username are empty, which is rare in practice.
Maintenance¶
Rotating the client secret¶
The azuread_application_password resource has a one-year end_date_relative. To rotate before expiry:
cd infrastructure/terraform/environments/staging
tofu taint module.salesforce_microsoft_signin.azuread_application_password.this
tofu apply -var-file=terraform.tfvars
tofu output -raw client_secret
Then update the Consumer Secret field in Setup → Auth. Providers → Microsoft.
Updating the Username Pattern¶
If the .smi suffix is removed in the future, update the username pattern in MicrosoftAuthRegistrationHandler.cls (and GoogleAuthRegistrationHandler.cls). Both handlers use the same pattern.
Performance¶
- Single SOQL query per login attempt.
- Indexed
Usernamefield;LIMIT 1.
Security Considerations¶
- Account Creation Prevention - Self-Registration must remain Disabled in the Experience Cloud site.
- Username matching - Username is immutable, so authentication is stable across email changes.
- Tenant audience -
commonallows any Microsoft account. The handler does not trust the tenant - it relies on the matching user already existing in Salesforce. - Client secret - stored only in Terraform/OpenTofu state (GCS, encrypted, versioned, ACL-restricted) and in Salesforce Auth Provider configuration. Never committed to source.
Related Documentation¶
- Microsoft Login Article - user-friendly overview
- Google Login Automation - sibling provider with the same matching strategy
- Infrastructure repo plan - Azure side
- Auth.RegistrationHandler Interface
Troubleshooting¶
Microsoft Button Not Appearing¶
- Verify the Microsoft Auth Provider is enabled for the Experience Cloud site.
- Verify the
microsoftLoginUrlgetter is wired up inCommunitiesLoginController. - Check browser console for errors.
Login Fails for Existing User¶
- Verify the user's Salesforce username follows the pattern
email@example.com.smi. - Check that
IsActive = trueon the user. - Review debug logs for the resolved email and the username pattern that was queried.
- If the Microsoft account has no verified email, confirm
data.username(UPN) matches the user's Salesforce username minus the.smisuffix - that's what the fallback uses.
Redirect URI Mismatch¶
If Azure rejects the callback with AADSTS50011: redirect URI mismatch:
- Compare the URL Salesforce shows in Setup → Auth Providers → Microsoft → Callback URL with
tofu output -raw client_id's app registration'sweb.redirect_uris. - Update
salesforce_callback_urlin the relevantterraform.tfvarsand re-apply.
Support¶
For issues or questions about Microsoft login automation:
- Email: webdev@spokanemountaineers.org
- GitHub Issues (Salesforce side): smi/issues
- GitHub Issues (Infrastructure side): infrastructure/issues