# Setup Auth

> Use when the user asks to "set up authentication", "add login", "add logout", "add sign in", "enable auth", "add role-based access", "add authorization", "protect routes", "configure identity provider", "configure Entra ID", "configure Entra External ID", "configure OpenID Connect", "add OIDC", "set up SAML", "set up WS-Federation", "set up local login", "add username password", "add Facebook login", "add Google sign in", "add Microsoft Account", "set up invitation login", or otherwise wants to set up authentication (login/logout) and role-based authorization for their Power Pages code site using any supported identity provider (Microsoft Entra ID, Entra External ID, OpenID Connect, SAML2, WS-Federation, local authentication, Microsoft Account, Facebook, or Google).

- Skill: `majiayu000/setup-auth` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/setup-auth`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/setup-auth/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/majiayu000/setup-auth

---


> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.

# Set Up Authentication & Authorization

Configure authentication (login/logout) and role-based authorization for a Power Pages code site. This skill supports multiple identity providers -- Microsoft Entra ID, Entra External ID (for customer-facing apps with self-service sign-up), OpenID Connect (generic), SAML2, WS-Federation, local authentication (username/password), Microsoft Account, Facebook, and Google. It also supports optional features including invitation-based registration and Terms & Conditions acceptance. Power Pages built-in 2FA is intentionally not scaffolded because the SendCode/VerifyCode pages are server-rendered and cannot be integrated into a SPA experience — use IdP-level MFA instead. It creates an auth service, type declarations, authorization utilities, auth UI components, and role-based access control patterns appropriate to the site's framework and chosen identity provider(s).

## Core Principles

- **Client-side auth is UX only** — Power Pages authentication is server-side (session cookies). Client-side role checks control what users see, not what they can access. Server-side table permissions enforce actual security.
- **Framework-appropriate patterns** — Every auth artifact (hooks, composables, services, directives, guards) must match the detected framework's idioms and conventions.
- **Development parity** — Include mock data for local development so developers can test auth flows and role-based UI without deploying to Power Pages.

**Initial request:** $ARGUMENTS

> **Prerequisites:**
>
> - An existing Power Pages code site created via `/create-site`
> - The site must be deployed at least once (`.powerpages-site` folder must exist)
> - Web roles must be created via `/create-webroles`

## Workflow

1. **Phase 1: Check Prerequisites** — Verify site exists, detect framework, check web roles
2. **Phase 2: Plan** — Gather auth requirements and present plan for approval
3. **Phase 3: Create Auth Service** — Auth service with login/logout and type declarations
4. **Phase 4: Create Authorization Utils** — Role-checking functions and wrapper components
5. **Phase 5: Create Auth UI** — Login/logout button integrated into navigation
6. **Phase 6: Implement Role-Based UI** — Apply role-based patterns to site components
7. **Phase 7: Verify Auth Setup** — Validate all auth files exist, build succeeds, auth UI renders
8. **Phase 8: Review & Deploy** — Summary and deployment prompt

---

## Phase 1: Check Prerequisites

**Goal:** Confirm the project exists, identify the framework, verify deployment status and web roles, and check for existing auth code.

### Actions

#### 1.1 Locate Project

Look for `powerpages.config.json` in the current directory or immediate subdirectories:

```text
**/powerpages.config.json
```

**If not found**: Tell the user to create a site first with `/create-site`.

#### 1.2 Detect Framework

Read `package.json` to determine the framework (React, Vue, Angular, or Astro). See `${CLAUDE_PLUGIN_ROOT}/references/framework-conventions.md` for the full framework detection mapping.

#### 1.3 Check Deployment Status

Look for the `.powerpages-site` folder:

```text
**/.powerpages-site
```

**If not found**: Tell the user the site must be deployed first:

> "The `.powerpages-site` folder was not found. The site needs to be deployed at least once before authentication can be configured."

Use `AskUserQuestion`:

| Question | Options |
|----------|---------|
| Your site needs to be deployed first. Would you like to deploy now? | Yes, deploy now (Recommended), No, I'll do it later |

**If "Yes, deploy now"**: Invoke `/deploy-site`, then resume.

**If "No"**: Stop — the site must be deployed first.

#### 1.4 Check Web Roles

Look for web role YAML files in `.powerpages-site/web-roles/`:

```text
**/.powerpages-site/web-roles/*.yml
```

Read each file and compile a list of existing web roles (name, id, flags).

**If no web roles exist**: Warn the user that web roles are needed for authorization. Ask if they want to create them first:

| Question | Options |
|----------|---------|
| No web roles were found. Web roles are required for role-based authorization. Would you like to create them now? | Yes, create web roles first (Recommended), Skip — I'll add roles later |

**If "Yes"**: Invoke `/create-webroles`, then resume.

**If "Skip"**: Continue — auth service and login/logout will still work, but role-based authorization will need roles created later.

#### 1.5 Discover Existing Auth Configuration

**Always run this discovery step, even on a first invocation** — the site may have site settings from a prior run, or from hand-editing the YAML files, even if no SPA auth code exists yet. The goal is to make sure we never silently drop a provider that's already configured server-side.

**Step 1 — Scan `.powerpages-site/site-settings/` for already-configured providers.**

Detect existing providers by matching site-setting filenames against these patterns:

| Pattern | Maps to provider type |
|---|---|
| `Authentication-OpenIdConnect-{Name}-AuthenticationType.sitesetting.yml` | OIDC (Entra External ID, Okta, Auth0, generic OIDC, B2C — all share the OIDC path) |
| `Authentication-SAML2-{Name}-AuthenticationType.sitesetting.yml` | SAML2 |
| `Authentication-WsFederation-{Name}-AuthenticationType.sitesetting.yml` | WS-Federation |
| `Authentication-OpenAuth-{Microsoft\|Facebook\|Google}-{ClientId\|AppId}.sitesetting.yml` | Social OAuth |
| `Authentication-Registration-LocalLoginEnabled.sitesetting.yml` with value `true` | Local Authentication |

For each detected provider, read its full set of `.sitesetting.yml` files to extract: `Authority` / `MetadataAddress`, `ClientId` / `AppId`, `AuthenticationType` (the providerIdentifier), `Caption` or display name (if present), and the `{Name}` slug used in the keys (e.g., `OpenIdConnect_1`, `EntraExternalId`).

**Distinguishing Entra ID variants from OIDC** — by Authority URL pattern:

| Authority pattern | Provider type | Notes |
|---|---|---|
| `https://login.windows.net/{guid}/` (no `/v2.0/`) — site's parent tenant | **Microsoft Entra ID (workforce)** — `type: 'entra-id'` | Auto-populated by Power Pages on site creation. The `{Name}` slug is usually `AzureAD`. **Set `providerIdentifier` to undefined in AUTH_PROVIDERS** — runtime resolver derives it from `Portal.tenant`. |
| `https://{subdomain}.ciamlogin.com/{tenantId}` (no trailing `/v2.0/`) | **Entra External ID** — `type: 'oidc'` | Customer tenant. Must include explicit `providerIdentifier` matching the Authority. |
| `https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/v2.0/{policy}` | **Azure AD B2C** (legacy) — `type: 'oidc'` | Older B2C product. Must include explicit `providerIdentifier`. |
| Any other OIDC authority (Okta, Auth0, Ping, etc.) | **OIDC (Generic)** — `type: 'oidc'` | Must include explicit `providerIdentifier`. |

**The Entra ID (workforce) case is special** — when Phase 1.5 discovery detects `Authentication/OpenIdConnect/AzureAD/*` settings on the site (which Power Pages auto-creates for the parent tenant), add a single entry to `EXISTING_PROVIDERS`:

```typescript
{
  id: 'entra-id',
  type: 'entra-id',
  displayName: existingCaption || 'Sign in with Microsoft',
  // NO providerIdentifier — resolveProviderIdentifier() derives it from Portal.tenant
}
```

Do NOT extract the tenant ID from the existing Authority site setting just to hardcode it back into AUTH_PROVIDERS — the runtime resolver handles it. This keeps the SPA code portable if the site is ever cloned to a different tenant.

**Step 2 — Scan for existing SPA auth code.**

Check for these files and read their key markers:

- `src/services/authService.ts` or `.js` — look for `AUTH_PROVIDERS` array (current pattern) vs single `AUTH_PROVIDER` constant (legacy)
- `src/types/powerPages.d.ts` — exists or not
- `src/utils/authorization.ts` — exists or not
- Auth components (`AuthButton.*`, `Login.*`, `Registration.*`, `RedeemInvitation.*`, etc.) — list which exist
- `src/pages/Login.tsx` — extract which providers it currently renders (via `AUTH_PROVIDERS` import or inline)

**Step 3 — Present findings to the user.**

If providers were detected from site settings, present them with their config:

```
I found these existing auth providers on your site:

  ✓ Entra External ID
    - ProviderName: OpenIdConnect_1
    - Tenant: ba275000-98c8-404d-a6f0-c5450f2aa668
    - ClientId: e728d63e-1190-495a-ae29-663e9cc10877
    - Configured in site settings: yes
    - Surfaced in SPA UI: NO (authService.ts has no entry for this provider)

  ✓ Local Authentication
    - LoginByEmail: true
    - Surfaced in SPA UI: yes
```

Use `AskUserQuestion`:

| Question | Header | Options |
|----------|--------|---------|
| I found existing auth providers on your site. What would you like to do? | Existing auth | Keep all existing providers and add a new one (Recommended) — preserves what's there, adds what you ask for next, Keep all existing providers (no new provider this run) — re-generates SPA code to surface what's already in site settings, Replace everything with a new configuration — wipes existing site settings and SPA code, starts fresh |

**"Keep all existing providers and add a new one"** (default path):
- Store the discovered providers as `EXISTING_PROVIDERS` — these will be merged into the `AUTH_PROVIDERS` array generated in Phase 3.2
- Phase 2.1 will prompt for the NEW provider being added; the existing ones are kept untouched
- For **local auth specifically** — if `Local Authentication` is in `EXISTING_PROVIDERS`, **always regenerate the local auth SPA code** (login flow, registration page, forgot/reset password, redeem invitation) from the user's Phase 2.1 answers. Don't try to preserve hand-edited local-auth code — the local flows are complex enough that partial updates introduce more bugs than they avoid.

**"Keep all existing providers (no new provider this run)"**:
- Skip the Phase 2.1 provider selection question entirely
- Re-derive `AUTH_PROVIDERS` from `EXISTING_PROVIDERS` only
- Useful for: fixing a site where the SPA UI is missing a provider that's already in site settings (the exact bug this branch was created to fix)

**"Replace everything with a new configuration"**:
- Set `EXISTING_PROVIDERS = []`
- Delete existing OIDC/SAML2/WsFed/OpenAuth site-setting YAMLs as part of Phase 8.1
- Run Phase 2.1 as if no providers existed

> **DO NOT** offer a "skip / no changes" option. If the user invokes setup-auth, they want auth set up — silently doing nothing is worse than asking.

### Output

- Project root path confirmed
- Framework identified (React, Vue, Angular, or Astro)
- Deployment status verified
- Web roles inventory compiled
- **`EXISTING_PROVIDERS` list compiled from site settings, with provider type, ProviderName slug, ClientId/Authority/etc. for each**
- **`MERGE_MODE` chosen: `keep-and-add` (default) | `keep-only` | `replace-all`**
- SPA auth file inventory recorded (which files exist, whether they use `AUTH_PROVIDERS` array or legacy single-provider pattern)

---

## Phase 2: Plan

**Goal:** Gather authentication requirements from the user and present the implementation plan for approval.

### Actions

#### 2.0 Smart Auth Inference (Before Asking)

Before asking the user which providers they want, analyze the site context from Phase 1 (site name, purpose, audience type) and try to infer appropriate auth settings automatically:

**Inference rules:**

| Site Type | Inferred Auth Settings | Rationale |
|-----------|----------------------|-----------|
| Internal/employee portal (HR, dashboard, admin) | Entra ID + invitation-only registration (`OpenRegistrationEnabled=false`, `InvitationEnabled=true`) | Internal sites should restrict access to invited employees only |
| Customer-facing portal (support, self-service) | Entra External ID + open registration | Customer portals need self-service sign-up for customers |
| Partner portal (B2B, vendor) | Entra ID + invitation-only registration | Partners are pre-vetted; open registration is a security risk |
| Public site with protected features (e-commerce, community) | Entra External ID + open registration + optional Google/Facebook | Public sites benefit from social login for frictionless sign-up |
| Loan/financial/banking portal | Entra External ID + invitation-only registration | Financial sites require controlled access for compliance |

**If you can infer with confidence**, present the recommendation with rationale:

> "Based on your site purpose ({purpose}), I recommend:
> - **{provider}** for authentication
> - **{registration mode}** because {rationale}
>
> Would you like to proceed with this configuration, or choose different providers?"

| Question | Options |
|----------|---------|
| Would you like to proceed with this recommended configuration? | Yes, proceed with recommendation, No, let me choose providers |

**If "Yes"**: Skip Phase 2.1 provider selection and proceed directly to collecting provider-specific details (ClientId, tenant name, etc.) for the recommended provider(s).

**If "No"** or **if you cannot infer with confidence**: Fall back to Phase 2.1 below.

#### 2.1 Gather Requirements

**Re-run handling — when Phase 1.5 detected existing providers:**

The behavior depends on the `MERGE_MODE` chosen in Phase 1.5:

- **`keep-only`** (user chose "keep all existing, no new provider this run") → Skip the new-provider selection question entirely. Proceed to the "Local Authentication" follow-ups only if local was detected. Phase 3.2 will generate `AUTH_PROVIDERS` from `EXISTING_PROVIDERS` only.
- **`keep-and-add`** (default — user wants to add one more) → Ask the user what to add. The provider selection question below should still be multi-select (the user could be adding multiple new providers in one go), but the existing providers are NOT in the list (they're already configured — the question is asking what's *new*). Common patterns:
  - User has Entra External ID, wants to add Local Auth → user selects "Local Authentication" → ask local follow-ups → Phase 3.2 merges
  - User has Entra External ID + Local, wants to add a *second* Entra External ID tenant → user selects "Entra External ID" → after collecting Authority/ClientId, ask: `"You already have an Entra External ID provider configured for tenant {existing-tenant}. This new one is a separate instance — give it a distinct ProviderName slug (used in site setting keys like Authentication/OpenIdConnect/{ProviderName}/* and in code as the provider id)."` Let the user pick a slug (default to the next incrementing number, e.g., `OpenIdConnect_2`) or pick a custom name (e.g., `EntraExternalId_Employee`).
- **`replace-all`** (user chose to wipe everything) → Run the provider selection question as on a first invocation.

**Do NOT proactively ask "do you want to configure multiple instances?"** at the start. Walk the user through configuring ONE provider at a time. When they finish configuring one and want another, they can re-run setup-auth → Phase 1.5 detects what's there → Phase 2.1 in `keep-and-add` mode asks "what do you want to add now?". This keeps the question count low for the common case (configure one provider) while still supporting the advanced case (multiple tenants).

**IMPORTANT: Multiple providers are supported.** The user may want more than one identity provider (e.g., Entra External ID + Google). If the user's initial prompt mentions specific providers, skip the provider selection question and proceed directly to collecting details for each mentioned provider.

> **IMPORTANT — Local Authentication:** NEVER set up local authentication by default. Do NOT include it in the provider selection list, do NOT recommend it in smart inference, and do NOT configure it unless the user explicitly and specifically asks for it (e.g., "I want username/password login", "set up local login", "add local auth"). External identity providers (Entra External ID, Entra ID, OIDC, etc.) are always preferred. If the user says something ambiguous like "add login", default to an external provider — never to local auth.

If the user has NOT specified which provider(s) they want, use `AskUserQuestion` to determine the identity provider(s). **This is a multi-select question** — the user can choose one or more:

| Question | Options |
|----------|---------|
| Which identity provider(s) do you want to use? (select all that apply) | Entra External ID (Recommended) — Customer identity with self-service sign-up (CIAM), Microsoft Entra ID — Azure AD / Entra ID for internal/employee sites, OpenID Connect (Generic) — Any OIDC-compliant provider (Okta, Auth0, Ping Identity, etc.), SAML2 — SAML 2.0 identity provider (ADFS, Shibboleth, etc.), WS-Federation — WS-Federation identity provider, Microsoft Account — Sign in with Microsoft personal/work account, Facebook — Sign in with Facebook, Google — Sign in with Google |

**Then, for EACH selected provider, ask the mandatory follow-up questions below.** Do not skip any provider — every selected provider needs its configuration collected before proceeding.

For each provider, also share the relevant Microsoft Learn documentation link so the user knows where to get the values:

**For "Microsoft Account"**:

| Question | Options |
|----------|---------|
| What is the Client ID from your Microsoft app registration? (e.g., `a1b2c3d4-e5f6-7890-abcd-ef1234567890`) | *(free text)* |

> Docs: https://learn.microsoft.com/en-us/power-pages/security/authentication/openid-settings

**For "Facebook"**:

| Question | Options |
|----------|---------|
| What is the App ID from the Facebook Developer Console? (e.g., `1234567890123456`) | *(free text)* |

> Docs: https://learn.microsoft.com/en-us/power-pages/security/authentication/facebook-settings

**For "Google"**:

| Question | Options |
|----------|---------|
| What is the Client ID from the Google Cloud Console? (e.g., `123456789-abc.apps.googleusercontent.com`) | *(free text)* |

> Docs: https://learn.microsoft.com/en-us/power-pages/security/authentication/openid-settings

**For "OpenID Connect (Generic)"**:

| Question | Options |
|----------|---------|
| What is the Authority URL for your OpenID Connect provider? (e.g., `https://dev-12345.okta.com/oauth2/default` or `https://login.microsoftonline.com/{tenant}/v2.0`) | *(free text)* |
| What is the Client ID (Application ID) from your provider's app registration? (e.g., `0oa1bcde2fGHIJklmn3o4`) | *(free text)* |
| What is the Metadata Address URL? (Only needed if your provider's metadata is NOT at `{authority}/.well-known/openid-configuration`). Leave blank to auto-derive. | *(free text, optional)* |
| What display name should the login button show? (e.g., `Sign in with Okta`) | *(free text)* |

> Docs: https://learn.microsoft.com/en-us/power-pages/security/authentication/openid-settings

**For "Entra External ID"** — use the 4-step walkthrough below. Do NOT just ask the user for Authority/ClientId/Metadata upfront — those values come from a tenant + app registration + user flow that the user may not have set up yet. Walk them through each prerequisite before asking for the corresponding value.

> Reference doc: https://learn.microsoft.com/en-us/power-pages/security/authentication/entra-external-id
> See also `${CLAUDE_PLUGIN_ROOT}/skills/setup-auth/references/authentication-reference.md` for the full Entra External ID prerequisites section the steps below cross-reference.

**Pre-computed values for THIS site** — before starting the walkthrough, compute:
- `SITE_URL` = the deployed site URL (e.g., `https://site-597pv.powerappsportals.com`). Read from `pac env who` + the site name, or from the site's existing settings.
- `PROVIDER_NAME` = if this is a fresh add, default to `OpenIdConnect_1` (or the next free `OpenIdConnect_N` slug per the CallbackPath uniqueness logic in Phase 8.1). The user can override to a custom slug like `EntraExternalId_Customer` for multi-instance setups.
- `REDIRECT_URI` = `{SITE_URL}/signin-{PROVIDER_NAME-lowercased}` — e.g., `https://site-597pv.powerappsportals.com/signin-openidconnect_1`. The user pastes this verbatim into the Entra app registration.
- `APP_NAME_SUGGESTION` = `power-pages-{site-shortname}` — e.g., `power-pages-savoria`
- `USER_FLOW_NAME_SUGGESTION` = `{site-shortname}-signupsignin` — e.g., `savoria-signupsignin`

Display these to the user before Step 1 so they have them handy.

##### Step 1 — Tenant

| Question | Header | Options |
|----------|--------|---------|
| Do you already have a Microsoft Entra External ID tenant? (This is a separate tenant type from a regular workforce Entra ID tenant — sometimes called CIAM.) | Tenant | Yes — I have an External ID tenant, No — help me create one (free 30-day trial), I'm not sure |

**If "No"**, show:

> Steps to create an Entra External ID tenant:
> 1. Open https://entra.microsoft.com/
> 2. Sign in with the account that should own the tenant
> 3. From the top, click **Manage tenants → Create**
> 4. Choose **External (for customers)** — NOT Workforce
> 5. Pick a domain prefix (the **tenant subdomain**) — e.g., `contoso` becomes `contoso.ciamlogin.com`. This appears in every login URL.
> 6. Free 30-day trial: no credit card required. You can attach a paid Azure subscription later.
>
> Detailed guide: https://learn.microsoft.com/en-us/entra/external-id/customers/quickstart-tenant-setup
>
> When you've created the tenant, switch to it (top-right tenant picker in entra.microsoft.com), then come back here.

**If "I'm not sure"**, show: "At https://entra.microsoft.com/ → top-right tenant picker. Tenants for customers are labeled **External**. Workforce tenants won't work — that's a different product."

Then collect the tenant identifiers:

| Question | Options |
|----------|---------|
| What is the tenant **subdomain**? (the part before `.ciamlogin.com` — e.g., `contoso`. Find it in the External ID tenant's Overview page under "Primary domain", removing `.onmicrosoft.com`.) | *(free text)* |
| What is the tenant **ID** (GUID)? (Find it in the External ID tenant's Overview page under "Tenant ID" — looks like `a1b2c3d4-e5f6-7890-abcd-ef1234567890`.) | *(free text)* |

**Validate**: subdomain matches `^[a-z0-9-]+$` (no dots, no uppercase, no `.ciamlogin.com` suffix); tenant ID matches the UUID regex. If either fails, show the expected format and re-prompt.

Store as `EXTERNAL_ID_TENANT_SUBDOMAIN` and `EXTERNAL_ID_TENANT_ID`.

##### Step 2 — App registration

**Confirm the Redirect URI first.** The skill pre-computes a default based on the site URL and `PROVIDER_NAME`, but the user may prefer a different URI:

> The Power Pages site needs a Redirect URI registered in your app registration. Based on the site URL and provider name, the default is:
>
> **`{REDIRECT_URI}`**
>
> You can keep this default, or use a different URI — for example, `{SITE_URL}/signin-entra-customer` or `{SITE_URL}/auth/external-id`. The host must be your Power Pages site; only the path can change.

| Question | Header | Options |
|----------|--------|---------|
| Use this Redirect URI? | Redirect URI | Use the default (Recommended) — `{REDIRECT_URI}`, Use a different URI |

**If "Use a different URI"**, ask:

| Question | Options |
|----------|---------|
| Enter the Redirect URI (must be on `{SITE_URL}`, must start with `{SITE_URL}/`, no spaces, no query string). Example: `{SITE_URL}/signin-entra-customer`. | *(free text)* |

**Validate** the custom URI:
- Must start with `{SITE_URL}/`
- Path portion must match `^/[a-zA-Z0-9_\-/]+$` (alphanumeric, hyphen, underscore, additional slashes allowed)
- Path must NOT collide with any `Authentication/OpenIdConnect/*/CallbackPath` already in `.powerpages-site/site-settings/` (from Phase 1.5 discovery)
- Path must NOT be a reserved Power Pages server path (`/Account/...`, `/SignIn`, `/Register`, `/_layout/...`, `/api/...`)

Re-prompt on invalid input. Then store the value as `REDIRECT_URI` for the rest of the walkthrough and Phase 8.1.

> **Note**: The skill writes two site settings derived from this single `REDIRECT_URI`: the user-facing `RedirectUri` (the full URI, sent to the IdP) and the internal `CallbackPath` (just the path portion, used by the OWIN middleware to know which incoming request to handle). The maker doesn't need to think about `CallbackPath` separately — the skill derives it automatically from `REDIRECT_URI` by extracting the path portion.

| Question | Header | Options |
|----------|--------|---------|
| Have you registered an app in your Entra External ID tenant for this Power Pages site? | App reg | No — walk me through it (Recommended for first time), Yes — I have the Application (client) ID |

**If "No"**, show step-by-step with the confirmed Redirect URI verbatim:

> Steps to register the app:
> 1. At https://entra.microsoft.com/, make sure you're in your External ID tenant (top-right picker)
> 2. **Applications → App registrations → New registration**
> 3. **Name**: `{APP_NAME_SUGGESTION}` (or your own name)
> 4. **Supported account types**: select **Accounts in this organizational directory only (single tenant)** — recommended for Power Pages. Multi-tenant configurations forcibly disable contact mapping by email for security.
> 5. **Redirect URI**: select **Web**, paste exactly:
>
>    ```
>    {REDIRECT_URI}
>    ```
>
>    (Copy this verbatim. Any mismatch between this value and the `RedirectUri` site setting causes sign-in to fail with `AADSTS50011: The reply URL specified in the request does not match`.)
> 6. Click **Register**
> 7. Open the **Authentication** tab → under "Implicit grant and hybrid flows" check **Access tokens** AND **ID tokens** → **Save**
> 8. Open the **API permissions** tab → click **Grant admin consent for {your tenant}** → confirm
> 9. Go back to the **Overview** tab and copy the **Application (client) ID** (it's a GUID)
>
> Detailed guide: https://learn.microsoft.com/en-us/entra/external-id/customers/quickstart-register-app

**If "Yes" (existing app)**, before asking for the Client ID, also confirm the user has the matching Redirect URI registered:

> Before continuing, please verify that your existing app registration has the following Redirect URI registered (under **Authentication → Web** in the Entra admin center):
>
> **`{REDIRECT_URI}`**
>
> If it's missing or different, add it now. An app registration can have multiple Web Redirect URIs registered — adding ours doesn't break any existing integrations. Sign-in will fail if the value in Power Pages doesn't match a registered URI exactly.

Then ask for the value:

| Question | Options |
|----------|---------|
| Paste the **Application (client) ID** from the Overview tab. | *(free text)* |

**Validate**: must match UUID v4 format (`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`). Re-prompt on mismatch.

Store as `EXTERNAL_ID_CLIENT_ID`.

**Do NOT ask about client secret.** Entra External ID app registrations are public clients using PKCE — no secret needed. The skill will create site settings without `ClientSecret` and skip Phase 8.1.1 (Key Vault) for this provider. If the user has a confidential-client scenario that requires a secret, they can add it manually via the Power Pages admin center after deploy — document this as an advanced override in Phase 8.5 post-deploy notes.

##### Step 3 — User flow

User flows define what attributes are collected from users and what claims appear in the ID token. Without one, sign-in fails after the IdP redirect.

| Question | Header | Options |
|----------|--------|---------|
| Have you created a sign-up/sign-in user flow in your Entra External ID tenant and attached it to your app? | User flow | No — walk me through it (Recommended for first time), Yes — I have the user flow name |

**If "No"**, the walkthrough's user-flow-attribute selection must match the `PROFILE_MAPPING_CHOICE` collected later (Track B's profile mapping question). Since this step runs BEFORE that question, ask it now (just for Entra External ID):

> The user flow needs to be told which attributes to collect from users and which claims to return in the token. The skill maps claims → Dataverse contact fields automatically — the attributes you select here determine what's available.

| Question | Header | Options |
|----------|--------|---------|
| What user profile info should the sign-up form collect and return as claims? | Profile attributes | Standard (Recommended) — Email, Given Name, Surname, Standard + phone — also Phone Number, Email only — minimal sign-up form |

Store as `PROFILE_ATTRIBUTES_CHOICE` (this also drives `PROFILE_MAPPING_CHOICE` in Track B — they should be consistent; default both to "Standard" unless the user explicitly differs).

Then show:

> Steps to create the user flow:
> 1. At https://entra.microsoft.com/, in your External ID tenant
> 2. **External Identities → User flows → New user flow**
> 3. **Name**: `{USER_FLOW_NAME_SUGGESTION}` (or your own — letters, digits, hyphens, underscores only)
> 4. **Identity providers** for sign-in: choose **Email with password** (Recommended — most familiar to customers) or **Email one-time passcode** (passwordless)
> 5. **User attributes to collect** (the sign-up form fields): based on your choice above, select:
>    - **Standard / Standard + phone**: ☑ Email Address, ☑ Given Name, ☑ Surname{`, ☑ Phone Number` if Standard + phone}
>    - **Email only**: ☑ Email Address
> 6. **User attributes to return as claims** (in the ID token): same selections as above — these power profile mapping into Dataverse contact fields
> 7. Click **Create**
> 8. Open the user flow you just created → **Applications** tab → **Add application** → select the app you registered in Step 2 → **Select**
>
> Detailed guide: https://learn.microsoft.com/en-us/entra/external-id/customers/how-to-user-flow-sign-up-sign-in-customers

Then ask:

| Question | Options |
|----------|---------|
| Paste the **user flow name** you created (e.g., `{USER_FLOW_NAME_SUGGESTION}`). | *(free text)* |

**Validate**: matches `^[a-zA-Z0-9_-]+$` (letters, digits, hyphens, underscores). Re-prompt on mismatch.

Store as `EXTERNAL_ID_USER_FLOW`.

##### Step 4 — Display name + Confirmation

| Question | Options |
|----------|---------|
| What should the login button label say? Default: **`Sign in with Entra External ID`** (shortened from "Sign in with Microsoft Entra External ID" so it fits on one line in the horizontal-row Login page layout — see note below). Do NOT use "Sign in with Microsoft" — that conflicts with the Microsoft Account social provider. | *(free text, defaulted)* |

> **Display name length guidance**: keep labels around **28 characters or less** to display on a single line in the horizontal-row Login page layout (which is the default). Longer labels still work — buttons grow vertically to wrap text to two lines — but single-line buttons look more polished. For reference:
> - "Sign in with Entra External ID" — 30 chars (wraps on narrow cards, fits on wider)
> - "Sign in with Microsoft Entra External ID" — 40 chars (wraps to two lines in the default horizontal layout)
> - "Customer Sign In" — 16 chars (always single line, but less descriptive)
>
> If the user has multiple external providers configured (e.g., Entra External ID + Google), shorter labels matter more because each button gets less width. For a single-provider site, longer labels are fine (the button spans the full row width).

Store as `EXTERNAL_ID_DISPLAY_NAME`.

Now derive the configuration and present a summary for confirmation:

- **Authority**: `https://{EXTERNAL_ID_TENANT_SUBDOMAIN}.ciamlogin.com/{EXTERNAL_ID_TENANT_ID}` (NO trailing `/v2.0/` — Entra External ID uses the bare tenant path, NOT the B2C-style URL)
- **MetadataAddress**: `https://{EXTERNAL_ID_TENANT_SUBDOMAIN}.ciamlogin.com/{EXTERNAL_ID_TENANT_ID}/v2.0/.well-known/openid-configuration`
- **AuthenticationType** (provider identifier in `AUTH_PROVIDERS` array and ExternalLogin POST): same value as Authority
- **RedirectUri**: `{REDIRECT_URI}` (computed earlier)
- **ClientId**: `{EXTERNAL_ID_CLIENT_ID}`

Present this summary inline:

> About to configure:
>
> | Field | Value |
> |---|---|
> | Provider | Microsoft Entra External ID |
> | Tenant | `{subdomain}.ciamlogin.com` (`{tenantId}`) |
> | App (Client) ID | `{clientId}` |
> | User flow | `{userFlowName}` |
> | Redirect URI | `{REDIRECT_URI}` (must already be registered in your app) |
> | Authority | `{authority}` (derived) |
> | Metadata | `{metadataAddress}` (derived) |
> | Display name | `{displayName}` |
> | Login button | "{displayName}" |
> | Client secret | None (public client / PKCE) |
>
> Continue to write these site settings?

| Question | Options |
|----------|---------|
| Continue? | Yes — write the site settings, No — let me adjust |

If "No", re-prompt for the specific value the user wants to change.

> **Implementation note:** Power Pages server treats Entra External ID as a generic OpenID Connect provider (no special CIAM handling). All settings go under `Authentication/OpenIdConnect/{ProviderName}/`. The `provider` value posted to `/Account/Login/ExternalLogin` must match the `AuthenticationType` site setting, which by default equals the authority URL.

**For "SAML2"**:

| Question | Options |
|----------|---------|
| What is the metadata endpoint URL for your SAML2 identity provider? (e.g., `https://adfs.contoso.com/FederationMetadata/2007-06/FederationMetadata.xml`) | *(free text)* |
| What display name should the login button show? (e.g., `Sign in with ADFS`) | *(free text)* |

> Docs: https://learn.microsoft.com/en-us/power-pages/security/authentication/saml2-settings

**For "WS-Federation"**:

| Question | Options |
|----------|---------|
| What is the metadata endpoint URL for your WS-Federation provider? (e.g., `https://adfs.contoso.com/federationmetadata/2007-06/federationmetadata.xml`) | *(free text)* |
| What is the provider realm or identifier? (e.g., `https://adfs.contoso.com/adfs/services/trust`) | *(free text)* |
| What display name should the login button show? (e.g., `Sign in with ADFS`) | *(free text)* |

> Docs: https://learn.microsoft.com/en-us/power-pages/security/authentication/ws-federation-settings

**Profile mapping (for every external provider — OIDC, Entra External ID, SAML2, WS-Federation, social)**

After collecting the provider's basic details, ask what user profile info should flow from the IdP to the Dataverse contact. **Don't skip this** — without it, contact records have empty `firstname`/`lastname` and the SPA falls back to displaying the email or username everywhere.

| Question | Header | Options |
|----------|--------|---------|
| What profile info should be copied from your identity provider into the Dataverse contact record? | Profile mapping | Standard (Recommended) — copy first name, last name, and email on first sign-in, Standard + phone — also copy mobile phone, Custom — let me pick which contact fields and claims to map, None — leave contact fields empty (the server will still populate emailaddress1 from the email claim) |

Store as `PROFILE_MAPPING_CHOICE`. Then ask:

| Question | Header | Options |
|----------|--------|---------|
| Should profile info be updated on every login, or only once at first sign-in? | Sync frequency | First sign-in only (Recommended) — copy claims once when the contact is created; let users edit their own profile afterwards without it being overwritten, Both — sync on first sign-in AND every login (use only when the IdP is the authoritative source of truth and you don't want users editing their profile in Power Pages) |

Store as `PROFILE_SYNC_FREQUENCY`. This determines whether to write `LoginClaimsMapping` (every login) in addition to `RegistrationClaimsMapping` (first sign-in only).

> **Why "First sign-in only" is now the default**: this skill optionally scaffolds a SPA profile page (`/user-profile`) where signed-in users can edit their own contact info. If `LoginClaimsMapping` is set, the server overwrites the user's edits with IdP claims on the very next login — which is confusing and silently undoes the user's work. "First sign-in only" lets the user own their profile after the contact is created. Switch to "Both" only when the IdP is the sole authoritative source for these fields (e.g., HR-managed workforce directory) and end-user edits should NOT persist.

**Claim type values** — the mapping format is comma-separated `contactfield=claimtype` (NOT JSON). For OIDC providers like Entra External ID, use OIDC short names:

| Choice | Generated mapping |
|---|---|
| Standard | `firstname=given_name,lastname=family_name,emailaddress1=email` |
| Standard + phone | `firstname=given_name,lastname=family_name,emailaddress1=email,mobilephone=phone_number` |
| Custom | Loop: ask the user for each `contactfield=claimtype` pair until they say done. Suggest OIDC short names (`given_name`, `family_name`, `email`, `phone_number`, `preferred_username`, custom claim names). Validate that `contactfield` is a known Dataverse contact column. |
| None | Don't write `RegistrationClaimsMapping` or `LoginClaimsMapping` settings. |

For **SAML2 / WS-Federation**, the claim types are URIs (e.g., `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname`). Adjust the "Standard" generated mapping accordingly. For **social** providers, the claim types are provider-specific (Google: `given_name`, Facebook: `name`).

**Contact linking (for every external provider)**

Ask whether to auto-link external sign-ins to existing contacts by email.

| Question | Header | Options |
|----------|--------|---------|
| If a user signs in with an external provider and their email matches an existing Dataverse contact, what should happen? | Contact linking | Link to the existing contact (Recommended) — auto-link by email match so makers don't end up with duplicate contacts when admins pre-create records (single-tenant providers only — see warning below), Create a new contact — always create a fresh contact, never auto-link (safer choice when the IdP doesn't verify emails) |

Store as `CONTACT_LINKING_CHOICE`. This drives `AllowContactMappingWithEmail` (`true` for "link", `false` for "create new").

> **Why "Link to the existing contact" is the default**: the common flow is that admins pre-create contact records in Dataverse (often via invitation or import) and then expect those exact contacts to be picked up when the user signs in for the first time via the configured IdP. Without linking, the server creates a brand-new contact and the pre-created record sits orphaned — confusing for makers and easy to misdiagnose. Linking by verified email is the well-known pattern for joining IdP identity to an existing CRM record.
>
> **⚠ Multi-tenant safety**: For **multi-tenant Entra External ID** (Authority uses `/organizations/` or `/common/`, or `IssuerFilter` is a wildcard), the Power Pages server **forcibly disables** `AllowContactMappingWithEmail` regardless of the site setting (`BlockContactMappingSettingForMultitenantApp` feature flag in `LoginController.cs:2578-2587`). Reason: email claims can't be trusted across tenants. If the user selects "Link to the existing contact" but the Authority is multi-tenant, warn them that linking won't work and recommend single-tenant Authority.
>
> **⚠ Security**: When `AllowContactMappingWithEmail = true`, an attacker who can sign into the configured IdP using a victim's email can take over the victim's contact. Enable only when the IdP verifies emails (Entra External ID with single tenant verifies; arbitrary OIDC may not). Switch to "Create a new contact" if you're configuring an IdP whose email-verification stance you don't control (e.g., a generic OIDC endpoint).

**For "Local Authentication"** (only if user explicitly requested it): Ask the user

…(truncated)
