Skip to content

Passkey Sample app#61

Open
yongdiw wants to merge 3 commits into
mainfrom
yongdi/add-passkey-sample
Open

Passkey Sample app#61
yongdiw wants to merge 3 commits into
mainfrom
yongdi/add-passkey-sample

Conversation

@yongdiw
Copy link
Copy Markdown
Collaborator

@yongdiw yongdiw commented May 5, 2026

A React + Vite single-page application demonstrating how to manage passkeys (FIDO2 credentials) for users in a Microsoft Entra External ID (CIAM) tenant.

The app uses MSAL.js for sign-in, enforces NGCMFA for passkey operations, and calls Microsoft Graph admin APIs to list, add, and delete a user's passkeys.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a React + Vite SPA sample that demonstrates passkey (FIDO2) management for Microsoft Entra External ID (CIAM), including MSAL sign-in, NGCMFA gating, and Microsoft Graph admin API integration.

Changes:

  • Added passkey management UI (list/add/delete) with supporting hooks, toasts, and styling.
  • Added Graph API client/service utilities (WebAuthn encoding helpers, Graph error parsing, passkey registration/deletion).
  • Added local-dev scaffolding: Vite HTTPS config, a local CORS proxy, and setup documentation.

Reviewed changes

Copilot reviewed 37 out of 39 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
passkey-sample/vite.config.js Configures Vite dev server (host/port/HTTPS) to support WebAuthn RP ID requirements during local dev.
passkey-sample/src/utils/tokenUtils.js Adds token parsing/acquisition helpers for MSAL tokens + app-token caching.
passkey-sample/src/utils/passkeyUtils.js Adds shared constants, retry timing helpers, NGCMFA expiry checks, and toast message factory.
passkey-sample/src/utils/graphServiceUtils.js Adds base64url/buffer conversions, formatting helpers, and Graph credential ID decoding.
passkey-sample/src/styles/index.css Adds baseline global typography styles.
passkey-sample/src/styles/App.css Adds app/page styling for navigation and passkey UI.
passkey-sample/src/services/PasskeyService.js Implements passkey creation options retrieval, WebAuthn credential creation, and Graph registration/deletion.
passkey-sample/src/services/GraphApiClient.js Adds a reusable Graph HTTP client with centralized error parsing.
passkey-sample/src/index.jsx Initializes MSAL, handles redirect completion, and renders the app.
passkey-sample/src/hooks/passkeys/usePasskeyFetcher.js Adds passkey fetch hook with retry logic and optional expected-change verification.
passkey-sample/src/hooks/passkeys/usePasskeyDeleteOperation.js Adds delete flow hook (modal + NGCMFA re-auth + refresh).
passkey-sample/src/hooks/passkeys/usePasskeyAddOperation.js Adds add flow hook (creationOptions + register + propagation delay + refresh).
passkey-sample/src/hooks/passkeys/useAuthentication.js Adds NGCMFA-related re-auth handling and operation caching across redirects.
passkey-sample/src/hooks/passkeys/index.js Barrel export for passkey hooks.
passkey-sample/src/components/passkeys/index.js Barrel export for passkey UI components.
passkey-sample/src/components/passkeys/components/utils.js Adds UI helper to parse device model strings for display.
passkey-sample/src/components/passkeys/components/PasskeysList.jsx Renders passkey list with loading/empty/error states.
passkey-sample/src/components/passkeys/components/PasskeysHeader.jsx Renders passkey section header + add button with count/limit.
passkey-sample/src/components/passkeys/components/PasskeyItem.jsx Renders passkey list item with delete + expandable details.
passkey-sample/src/components/passkeys/components/PasskeyDetails.jsx Renders expanded passkey details.
passkey-sample/src/components/passkeys/components/DeleteModal.jsx Adds confirmation modal for passkey deletion.
passkey-sample/src/components/passkeys/PasskeysSection.jsx Composes hooks + UI into the main passkey management section.
passkey-sample/src/components/common/index.js Barrel export for shared UI components.
passkey-sample/src/components/common/UIComponents.jsx Adds common UI components (SecurityAlert, UserProfileHeader).
passkey-sample/src/components/common/ToastNotifications.jsx Adds toast container with special handling for session-expired action toasts.
passkey-sample/src/components/SecurityPage.jsx Main authenticated page: token acquisition, NGCMFA tracking, and passkeys section orchestration.
passkey-sample/src/components/PageLayout.jsx Adds shared layout + unauthenticated welcome message.
passkey-sample/src/components/NavigationBar.jsx Adds nav with sign-in/sign-out actions and token cache clearing on logout.
passkey-sample/src/authConfig.js Adds MSAL config and app token proxy configuration.
passkey-sample/src/App.jsx Sets up MSAL provider + authenticated page rendering.
passkey-sample/public/manifest.json Adds PWA manifest metadata.
passkey-sample/public/favicon.svg Adds app icon asset.
passkey-sample/package.json Adds dependencies/scripts for Vite + React + MSAL + Bootstrap and the local proxy script.
passkey-sample/index.html Adds Vite HTML entry with manifest/favicon wiring.
passkey-sample/cors.js Adds local CORS proxy for token endpoint calls needed by client-credentials flow in-browser.
passkey-sample/README.md Adds setup docs for local HTTPS/RP ID, Entra config, and running the sample.
passkey-sample/.gitignore Ignores build artifacts, node_modules, certs, and local env.
passkey-sample/.env.example Provides template env vars for host/port and HTTPS cert paths.
Files not reviewed (1)
  • passkey-sample/package-lock.json: Language not supported
Comments suppressed due to low confidence (7)

passkey-sample/src/utils/tokenUtils.js:1

  • getAccessToken documents/uses a { token, decodedToken, error } return shape, but in the MFA-expired branch it returns acquireTokenRedirect(...) (MSAL returns Promise<void> for redirect). Callers that do result.error will crash when this path is hit. Fix by keeping the return type consistent (e.g., trigger redirect and then return { token:null, decodedToken:null, error:'Redirecting...' }, or throw a sentinel error that callers handle), rather than returning the redirect promise.
    passkey-sample/src/services/PasskeyService.js:1
  • This assumes creationOptions.excludeCredentials is always present and an array. WebAuthn creation options may omit excludeCredentials, which would throw here. Fix by defaulting to an empty array and only mapping when it’s an array; also consider avoiding mutation of the creationOptions argument (create a new array and use it in publicKey).
    passkey-sample/src/services/GraphApiClient.js:1
  • Accept-Encoding is a forbidden request header in browsers and cannot be set reliably; it should be removed. Also, setting Content-Type: application/json on GET/DELETE requests (with no body) can trigger unnecessary CORS preflights; consider only setting Content-Type when a request body is present.
    passkey-sample/vite.config.js:1
  • The comment says WebSocket-based HMR is disabled in favor of polling, but the config still explicitly uses wss HMR (which is WebSocket-based). Either update the comment to reflect what’s actually configured, or change the configuration to a polling-based alternative (e.g., enabling server.watch.usePolling and/or disabling HMR when hasSSL).
    passkey-sample/src/utils/passkeyUtils.js:1
  • The JSDoc claims “exponential backoff”, but the implementation is linear (baseDelay * attempt). Either update the docs to say linear backoff, or change the delay calculation to exponential (e.g., baseDelay * 2^(attempt-1)) to match the comment.
    passkey-sample/src/styles/App.css:1
  • There are duplicated selectors for .iconText, .navbarStyle, and .navbarButton, with conflicting declarations (e.g., .navbarStyle padding with and without !important). This makes style behavior harder to reason about. Consolidate each selector to a single definition and remove redundant/conflicting declarations.
    passkey-sample/src/services/PasskeyService.js:1
  • fetchUserPasskey returns a collection of passkeys, but the name is singular. Rename to fetchUserPasskeys (or similar) to match the return value and avoid confusion at call sites.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread passkey-sample/cors.js
Comment thread passkey-sample/src/components/PageLayout.jsx
Comment thread passkey-sample/README.md
Comment thread passkey-sample/src/components/common/UIComponents.jsx
Comment thread passkey-sample/README.md Outdated

This is a React Single Page Application (SPA) that demonstrates authentication with Microsoft Identity Platform and passkey management using Microsoft Graph API.

**⚠️ This sample app is currently under private preview for testing purpose. Please do not deploy to production environment.**
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not longer under private preview. This statement should be updated. Please work with Namrata.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 312be3c — dropped the "private preview" qualifier per Namrata. Now reads: "⚠️ This sample app is for testing purpose. Please do not deploy to production environment."

Comment thread passkey-sample/README.md
Comment on lines +150 to +183
clientId: "<your-client-id-here>", // Application (client) ID from app registration
authority: "https://passkeytest.ciamlogin.com/", // Replace passkeytest with your tenant subdomain
redirectUri: "/", // Resolved at runtime to the registered redirect URI
},
// ... rest of configuration
};
```

**How to get these values:**

1. **Client ID**: Found in your app registration overview page
2. **Authority**: Your CIAM tenant authority URL in the format `https://passkeytest.ciamlogin.com/` (replace `passkeytest` with your tenant subdomain)
3. **Redirect URI**: The URL where users will be redirected after authentication **(must be registered in Entra portal)**

#### Step 2: Environment Configuration (.env file)

The repository does **not** include a `.env` file — you need to create one yourself. Copy `.env.example` to a new file named `.env` in the same `sample/` folder and fill in your values. `.env` is gitignored, so your local copy stays on your machine.

```env
# Local dev hostname — must match the auth.<tenant>.ciamlogin.com subdomain in your hosts file
VITE_HOST=auth.passkeytest.ciamlogin.com
VITE_PORT=3000

# SSL certificate filenames at the project root
VITE_SSL_CERT=auth-cert.pem
VITE_SSL_KEY=auth-key.pem
```

#### Step 3: Application Configuration (authConfig.js)

The React app authentication configuration is now centralized in `src/authConfig.js`. Update the `appConfig` object with your values:

```javascript
export const appConfig = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing appSecret directly in authConfig.js could expose the secret when committed randomly. Recommend reading the value from .env file and configure .gitignore to not commit this file accidentally. ReadMe should guide how to update .env file.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Suresh — addressed in 759964a. The appSecret is no longer in authConfig.js; the SPA now reads it directly from VITE_APP_SECRET in the local .env file at the only call site (SecurityPage.jsx). I extended .env.example with VITE_APP_SECRET= and updated Step 2: Environment Configuration (.env file) in the README so users know to set their secret there. .env is already gitignored, so it won't be committed.

Comment thread passkey-sample/README.md Outdated

#### Step 3: Application Configuration (authConfig.js)

The React app authentication configuration is now centralized in `src/authConfig.js`. Update the `appConfig` object with your values:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does authconfig.js gets bundled into the app and how is it prevented from sending to the browser at the client side?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. This README walks through running the SPA locally for demo and testing — because it's a Vite SPA, anything it imports (including any VITE_* env var) is bundled into the client JavaScript at build time and visible in the browser. That's expected for a local-dev sample, which is why the README's security warning calls it out as local-dev only.

For production deployment, the appSecret must never live in the SPA. Guidance is in the existing Passkeys deployment doc from private preview: entra-previews/docs/Passkeys/react-spa-sign-in-passkey.md (PP5), which recommends a confidential-client backend that retrieves the secret from Azure Key Vault (or equivalent) and performs the token exchange server-side, so the browser only sees the resulting app token via a proxied endpoint. That doc was authored during private preview and should be updated for GA.

yongdiw and others added 2 commits May 20, 2026 11:58
- Remove hardcoded appSecret from authConfig.js; read VITE_APP_SECRET from .env in SecurityPage.jsx (the only caller). Keeps authConfig.js Node-safe for cors.js.

- Add VITE_APP_SECRET to .env.example.

- Add customDomain field to appConfig (already consumed in PasskeyService.js) to match README.

- Update README Step 2/3 to document VITE_APP_SECRET and remove hardcoded secret from the config snippet.

- Sync package-lock.json name with package.json.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

@sureshagit sureshagit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with minor comment in readme.

Comment thread passkey-sample/README.md
@@ -0,0 +1,369 @@
# Microsoft Identity Platform - React SPA with Passkeys

This is a React Single Page Application (SPA) that demonstrates authentication with Microsoft Identity Platform and passkey management using Microsoft Graph API.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the sample app have server backend and FE browser react for this app?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified offline. It is a single page app to show functionality for the sample app.

Comment thread passkey-sample/README.md

The following instructions set up this sample app **locally**. Throughout this guide, `passkeytest.ciamlogin.com` is used as the example tenant — replace it with your own.

### 1. Windows Domain Setup (Required for Passkey rp.id Compliance)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rp id are the custom domains configured in their tenant. Is there a guidance on deploying SPA on custom domains?

Comment thread passkey-sample/README.md

### 2. Tenant Configuration

#### Step 1: Register Redirect URI in Entra Portal
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a critical step to only register this app in their test tenant. Also, make sure the app targets the single tenant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants