swa --version → 2.0.9 (verified latest at time of filing).
- Yes, I am accessing the CLI from port
:4280.
- Debug logs were collected with
--verbose=silly and confirm the behavior described below.
Describe the bug
When using a real Microsoft Entra ID tenant via the azureActiveDirectory identity provider (i.e. not the built-in mock auth), the local emulator behaves differently from production Azure Static Web Apps in three ways. All three are in dist/msha/auth/routes/:
loginParameters from staticwebapp.config.json are ignored — the AAD authorize URL is built with a hardcoded scope=openid+profile+email and no prompt, domain_hint, login_hint, or extra scope is forwarded.
clientPrincipal.userId is empty for AAD users — the callback handler reads user["id"] / data.id, but AAD's OIDC /userinfo returns the user id as sub (per OIDC spec). Additionally, userId is not even included in the principal object literal that is returned.
clientPrincipal.userDetails is empty for AAD users whose userinfo response lacks email/login — there is no fallback to preferred_username, which is the standard AAD claim.
Each one breaks an app that works correctly when deployed.
To Reproduce
- Create a project with a
staticwebapp.config.json configured for a real Entra ID tenant:
{
"auth": {
"identityProviders": {
"azureActiveDirectory": {
"registration": {
"openIdIssuer": "https://login.microsoftonline.com/<tenant>/v2.0",
"clientIdSettingName": "AZURE_CLIENT_ID",
"clientSecretSettingName": "AZURE_CLIENT_SECRET"
},
"login": {
"loginParameters": [
"scope=openid profile email api://<api>/Backend.Access",
"prompt=select_account"
]
}
}
}
},
"routes": [{ "route": "/*", "allowedRoles": ["authenticated"] }]
}
- Set
AZURE_CLIENT_ID / AZURE_CLIENT_SECRET env vars to a valid AAD app registration (Web platform, redirect URI http://localhost:4280/.auth/login/aad/callback).
- Run:
swa start http://localhost:5173 --run "npm run dev" --api-location http://localhost:7217 --verbose=silly
- Open
http://localhost:4280/.auth/login/aad in a fresh browser session.
- Inspect the redirect URL to
login.microsoftonline.com/.../authorize.
- Complete login, then
GET http://localhost:4280/.auth/me.
Expected behavior
- The authorize URL contains every entry from
loginParameters (matching what deployed SWA does), e.g. prompt=select_account, scope=openid profile email api://.../Backend.Access, etc.
clientPrincipal.userId is populated with the AAD sub claim.
clientPrincipal.userDetails is populated even when only preferred_username is present.
In short, /.auth/me should return the same clientPrincipal shape as deployed SWA so backends consuming x-ms-client-principal behave the same locally and in production.
Actual behavior
- The authorize URL is:
https://login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize?response_type=code&client_id=<clientId>&redirect_uri=http://localhost:4280/.auth/login/aad/callback&scope=openid+profile+email&state=<state>
loginParameters are silently dropped.
2. clientPrincipal.userId is missing (the field is omitted because the value resolves to undefined).
3. clientPrincipal.userDetails is empty for AAD users without email/login in userinfo.
Suggested fix (drop-in diff against 2.0.9)
Happy to open a PR against main with these changes plus tests, if maintainers are interested.
diff --git a/dist/msha/auth/routes/auth-login-provider-callback.js b/dist/msha/auth/routes/auth-login-provider-callback.js
@@
- const userDetails = user["login"] || user["email"] || user?.data?.["username"];
+ const userDetails = user["login"] || user["email"] || user?.data?.["username"] || user["preferred_username"];
const name = user["name"] || user?.data?.["name"];
const givenName = user["given_name"];
const familyName = user["family_name"];
const picture = user["picture"];
- const userId = user["id"] || user?.data?.["id"];
+ // AAD's `/oidc/userinfo` returns the user id as `sub`, not `id`.
+ const userId = user["id"] || user?.data?.["id"] || user["sub"];
@@
return {
identityProvider: authProvider,
+ userId,
userDetails,
claims,
userRoles: ["authenticated", "anonymous"],
diff --git a/dist/msha/auth/routes/auth-login-provider-custom.js b/dist/msha/auth/routes/auth-login-provider-custom.js
@@
- case "aad":
+ case "aad": {
const authorizationEndpoint = await new OpenIdHelper(authFields?.openIdIssuer, authFields?.clientIdSettingName).getAuthorizationEndpoint();
- location = `${authorizationEndpoint}?response_type=code&client_id=${authFields?.clientIdSettingName}&redirect_uri=${redirectUri}/.auth/login/aad/callback&scope=openid+profile+email&state=${hashedState}`;
+ const aadLoginCfg = customAuth?.identityProviders?.[ENTRAID_FULL_NAME]?.login;
+ const aadLoginParams = Array.isArray(aadLoginCfg?.loginParameters) ? aadLoginCfg.loginParameters : [];
+ let aadScope = "openid profile email";
+ const aadExtra = [];
+ for (const p of aadLoginParams) {
+ if (typeof p !== "string") continue;
+ if (p.startsWith("scope=")) {
+ aadScope = p.substring("scope=".length);
+ } else {
+ aadExtra.push(p);
+ }
+ }
+ location = `${authorizationEndpoint}?response_type=code&client_id=${authFields?.clientIdSettingName}&redirect_uri=${redirectUri}/.auth/login/aad/callback&scope=${encodeURIComponent(aadScope)}&state=${hashedState}`;
+ if (aadExtra.length) {
+ location += "&" + aadExtra.join("&");
+ }
break;
+ }
Screenshots
Not applicable — bugs are visible in the redirect URL and in the JSON returned by /.auth/me (text only).
Desktop
- OS: Windows 11
- Node.js: v22.22.3
@azure/static-web-apps-cli: 2.0.9
- Azure Functions Core Tools: 4.10.0
- Browser tested: Edge / Chrome (behavior identical; the bugs are server-side in the emulator)
Additional context
- The Azure Functions backend in this project is
dotnet-isolated on net8.0 and consumes the x-ms-client-principal header. The principal-shape mismatch (bugs 2 and 3) forces the backend to add a fallback parser for userId purely to work around the local emulator — something that should not be necessary if the emulator matches production.
- All three fixes have been running in production-equivalent local development for several weeks via
patch-package against node_modules/@azure/static-web-apps-cli. No regressions observed for other identity providers (GitHub, Twitter, Google, Facebook) — the aad change is scoped to case "aad":, and the userId / userDetails changes are pure additional fallbacks.
- Related issues (if maintainers want to triage together):
Azure/static-web-apps#1123 (federated logout / SSO behavior) — adjacent area but a different code path.
swa --version→2.0.9(verified latest at time of filing).:4280.--verbose=sillyand confirm the behavior described below.Describe the bug
When using a real Microsoft Entra ID tenant via the
azureActiveDirectoryidentity provider (i.e. not the built-in mock auth), the local emulator behaves differently from production Azure Static Web Apps in three ways. All three are indist/msha/auth/routes/:loginParametersfromstaticwebapp.config.jsonare ignored — the AAD authorize URL is built with a hardcodedscope=openid+profile+emailand noprompt,domain_hint,login_hint, or extrascopeis forwarded.clientPrincipal.userIdis empty for AAD users — the callback handler readsuser["id"]/data.id, but AAD's OIDC/userinforeturns the user id assub(per OIDC spec). Additionally,userIdis not even included in the principal object literal that is returned.clientPrincipal.userDetailsis empty for AAD users whose userinfo response lacksemail/login— there is no fallback topreferred_username, which is the standard AAD claim.Each one breaks an app that works correctly when deployed.
To Reproduce
staticwebapp.config.jsonconfigured for a real Entra ID tenant:{ "auth": { "identityProviders": { "azureActiveDirectory": { "registration": { "openIdIssuer": "https://login.microsoftonline.com/<tenant>/v2.0", "clientIdSettingName": "AZURE_CLIENT_ID", "clientSecretSettingName": "AZURE_CLIENT_SECRET" }, "login": { "loginParameters": [ "scope=openid profile email api://<api>/Backend.Access", "prompt=select_account" ] } } } }, "routes": [{ "route": "/*", "allowedRoles": ["authenticated"] }] }AZURE_CLIENT_ID/AZURE_CLIENT_SECRETenv vars to a valid AAD app registration (Web platform, redirect URIhttp://localhost:4280/.auth/login/aad/callback).swa start http://localhost:5173 --run "npm run dev" --api-location http://localhost:7217 --verbose=sillyhttp://localhost:4280/.auth/login/aadin a fresh browser session.login.microsoftonline.com/.../authorize.GET http://localhost:4280/.auth/me.Expected behavior
loginParameters(matching what deployed SWA does), e.g.prompt=select_account,scope=openid profile email api://.../Backend.Access, etc.clientPrincipal.userIdis populated with the AADsubclaim.clientPrincipal.userDetailsis populated even when onlypreferred_usernameis present.In short,
/.auth/meshould return the sameclientPrincipalshape as deployed SWA so backends consumingx-ms-client-principalbehave the same locally and in production.Actual behavior
loginParametersare silently dropped.2.
clientPrincipal.userIdis missing (the field is omitted because the value resolves toundefined).3.
clientPrincipal.userDetailsis empty for AAD users withoutemail/loginin userinfo.Suggested fix (drop-in diff against
2.0.9)Happy to open a PR against
mainwith these changes plus tests, if maintainers are interested.Screenshots
Not applicable — bugs are visible in the redirect URL and in the JSON returned by
/.auth/me(text only).Desktop
@azure/static-web-apps-cli: 2.0.9Additional context
dotnet-isolatedonnet8.0and consumes thex-ms-client-principalheader. The principal-shape mismatch (bugs 2 and 3) forces the backend to add a fallback parser foruserIdpurely to work around the local emulator — something that should not be necessary if the emulator matches production.patch-packageagainstnode_modules/@azure/static-web-apps-cli. No regressions observed for other identity providers (GitHub, Twitter, Google, Facebook) — theaadchange is scoped tocase "aad":, and theuserId/userDetailschanges are pure additional fallbacks.Azure/static-web-apps#1123(federated logout / SSO behavior) — adjacent area but a different code path.