Passkey Sample app#61
Conversation
There was a problem hiding this comment.
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
getAccessTokendocuments/uses a{ token, decodedToken, error }return shape, but in the MFA-expired branch it returnsacquireTokenRedirect(...)(MSAL returnsPromise<void>for redirect). Callers that doresult.errorwill 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.excludeCredentialsis always present and an array. WebAuthn creation options may omitexcludeCredentials, which would throw here. Fix by defaulting to an empty array and only mapping when it’s an array; also consider avoiding mutation of thecreationOptionsargument (create a new array and use it inpublicKey).
passkey-sample/src/services/GraphApiClient.js:1 Accept-Encodingis a forbidden request header in browsers and cannot be set reliably; it should be removed. Also, settingContent-Type: application/jsonon GET/DELETE requests (with no body) can trigger unnecessary CORS preflights; consider only settingContent-Typewhen 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
wssHMR (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., enablingserver.watch.usePollingand/or disabling HMR whenhasSSL).
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.,.navbarStylepadding 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 fetchUserPasskeyreturns a collection of passkeys, but the name is singular. Rename tofetchUserPasskeys(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.
|
|
||
| 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.** |
There was a problem hiding this comment.
It is not longer under private preview. This statement should be updated. Please work with Namrata.
There was a problem hiding this comment.
Updated in 312be3c — dropped the "private preview" qualifier per Namrata. Now reads: "
| 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 = { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| #### 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: |
There was a problem hiding this comment.
Does authconfig.js gets bundled into the app and how is it prevented from sending to the browser at the client side?
There was a problem hiding this comment.
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.
- 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>
sureshagit
left a comment
There was a problem hiding this comment.
Approved with minor comment in readme.
| @@ -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. | |||
There was a problem hiding this comment.
Does the sample app have server backend and FE browser react for this app?
There was a problem hiding this comment.
Clarified offline. It is a single page app to show functionality for the sample app.
|
|
||
| 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) |
There was a problem hiding this comment.
rp id are the custom domains configured in their tenant. Is there a guidance on deploying SPA on custom domains?
|
|
||
| ### 2. Tenant Configuration | ||
|
|
||
| #### Step 1: Register Redirect URI in Entra Portal |
There was a problem hiding this comment.
Add a critical step to only register this app in their test tenant. Also, make sure the app targets the single tenant.
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.