Azure / Azure/static-web-apps-cli

Local AAD auth emulator: loginParameters ignored, userId missing, userDetails empty (preferred_username not used)

Open
#997 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
668
Forks
156
PR merge metrics
No merged PRs in 30d

Description

- `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/`:
1. **`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.
2. **`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.
3. **`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
1. Create a project with a `staticwebapp.config.json` configured for a real Entra ID tenant:
```json
{
"auth": {
"identityProviders": {
"azureActiveDirectory": {
"registration": {
"openIdIssuer": "https://login.microsoftonline.com//v2.0",
"clientIdSettingName": "AZURE_CLIENT_ID",
"clientSecretSettingName": "AZURE_CLIENT_SECRET"
},
"login": {
"loginParameters": [
"scope=openid profile email api:///Backend.Access",
"prompt=select_account"
]
}
}
}
},
"routes": [{ "route": "/*", "allowedRoles": ["authenticated"] }]
}
```
2. 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`).
3. Run:
```bash
swa start http://localhost:5173 --run "npm run dev" --api-location http://localhost:7217 --verbose=silly
```
4. Open `http://localhost:4280/.auth/login/aad` in a fresh browser session.
5. Inspect the redirect URL to `login.microsoftonline.com/.../authorize`.
6. Complete login, then `GET http://localhost:4280/.auth/me`.
### Expected behavior
1. 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.
2. `clientPrincipal.userId` is populated with the AAD `sub` claim.
3. `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
1. The authorize URL is:
```
https://login.microsoftonline.com//oauth2/v2.0/authorize?response_type=code&client_id=&redirect_uri=http://localhost:4280/.auth/login/aad/callback&scope=openid+profile+email&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
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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.