feat: add Passkey and MyAccount API support#927
Conversation
d1ea74c to
ef39e17
Compare
📝 WalkthroughWalkthroughThis PR adds Passkey (WebAuthn) and MyAccount API support to AuthService via new Observable client interfaces and implementations, exports corresponding types from public-api.ts, bumps the ChangesPasskey and MyAccount API Implementation
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Component
participant AuthService
participant Auth0Client
participant AuthState
Component->>AuthService: passkey.signup(options)
AuthService->>Auth0Client: auth0Client.passkey.signup(options)
Auth0Client-->>AuthService: TokenEndpointResponse
AuthService->>AuthState: refresh()
AuthService-->>Component: emit TokenEndpointResponse
Component->>AuthService: myAccount.enrollmentChallenge(options)
AuthService->>Auth0Client: auth0Client.myAccount.enrollmentChallenge(options)
Auth0Client-->>AuthService: EnrollmentChallengeResponse
AuthService-->>Component: emit EnrollmentChallengeResponse
Related PRs: None identified from the provided data. Suggested labels: enhancement, documentation Suggested reviewers: none identified from the provided data. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
projects/auth0-angular/src/lib/auth.service.ts (2)
530-545: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winInconsistent error-state handling vs. other token-issuing flows.
passkey.signup/loginupdateauthStateon success but don't callauthState.setError()on failure, unlikegetAccessTokenSilently,getAccessTokenWithPopup, andloginWithCustomTokenExchangein this same file, which all callthis.authState.setError(error)andthis.authState.refresh()in acatchErrorbefore rethrowing. Since passkey login/signup is a full authentication flow, callers relying onerror$won't see passkey failures reflected there.♻️ Suggested alignment with existing error-handling pattern
readonly passkey: ObservablePasskeyApiClient = { signup: (options: PasskeySignupOptions) => - from( - this.auth0Client.passkey.signup(options).then((tokenResponse) => { - this.authState.refresh(); - return tokenResponse; - }) - ), + of(this.auth0Client).pipe( + concatMap((client) => client.passkey.signup(options)), + tap(() => this.authState.refresh()), + catchError((error) => { + this.authState.setError(error); + this.authState.refresh(); + return throwError(error); + }) + ), login: (options?: PasskeyLoginOptions) => - from( - this.auth0Client.passkey.login(options).then((tokenResponse) => { - this.authState.refresh(); - return tokenResponse; - }) - ), + of(this.auth0Client).pipe( + concatMap((client) => client.passkey.login(options)), + tap(() => this.authState.refresh()), + catchError((error) => { + this.authState.setError(error); + this.authState.refresh(); + return throwError(error); + }) + ), };Note:
loginWithPopupfollows the same lax pattern as passkey today (nosetErroron failure), so this is an existing inconsistency in the file rather than one introduced solely by this PR; flagging since passkey/myAccount is new surface area worth aligning with the newer, more complete pattern.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@projects/auth0-angular/src/lib/auth.service.ts` around lines 530 - 545, The passkey API methods in auth.service.ts follow the success path but do not mirror the existing error-state pattern used by getAccessTokenSilently, getAccessTokenWithPopup, and loginWithCustomTokenExchange. Update the passkey.signup and passkey.login implementations to catch failures, call this.authState.setError(error), trigger this.authState.refresh(), and then rethrow so error$ reflects passkey authentication errors consistently.
530-545: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoffEager side-effect execution breaks Observable laziness contract.
this.auth0Client.passkey.signup(options)(andlogin) executes immediately when the factory arrow function runs, before any subscription occurs —from()only wraps an already-started promise. Consumers expecting standard Observable semantics (deferred execution untilsubscribe()) may be surprised if they construct the Observable without immediately subscribing (e.g., pass it throughshare(), store it, or use it incombineLatest), since the WebAuthn ceremony would already have started.This mirrors the pre-existing
mfaclient and several other methods (loginWithRedirect,loginWithPopup) in this file, so it's a known convention rather than a new regression — buthandleRedirectCallback,getAccessTokenSilently, andloginWithCustomTokenExchangealready usedefer/concatMapfor true laziness. Consider aligning new APIs with the safer pattern going forward.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@projects/auth0-angular/src/lib/auth.service.ts` around lines 530 - 545, The passkey methods in auth.service.ts start the WebAuthn promise eagerly, which breaks Observable laziness. Update the ObservablePasskeyApiClient implementation for passkey.signup and passkey.login to defer the call until subscription, using the same lazy pattern already used by handleRedirectCallback/getAccessTokenSilently/loginWithCustomTokenExchange; keep authState.refresh() inside the deferred async flow so it runs only after the underlying promise resolves.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@projects/auth0-angular/src/lib/auth.service.ts`:
- Around line 530-545: The passkey API methods in auth.service.ts follow the
success path but do not mirror the existing error-state pattern used by
getAccessTokenSilently, getAccessTokenWithPopup, and
loginWithCustomTokenExchange. Update the passkey.signup and passkey.login
implementations to catch failures, call this.authState.setError(error), trigger
this.authState.refresh(), and then rethrow so error$ reflects passkey
authentication errors consistently.
- Around line 530-545: The passkey methods in auth.service.ts start the WebAuthn
promise eagerly, which breaks Observable laziness. Update the
ObservablePasskeyApiClient implementation for passkey.signup and passkey.login
to defer the call until subscription, using the same lazy pattern already used
by handleRedirectCallback/getAccessTokenSilently/loginWithCustomTokenExchange;
keep authState.refresh() inside the deferred async flow so it runs only after
the underlying promise resolves.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b5576dca-753e-491c-9ee5-001e0c24b240
📒 Files selected for processing (7)
EXAMPLES.mdpackage.jsonprojects/auth0-angular/package.jsonprojects/auth0-angular/src/lib/auth.service.spec.tsprojects/auth0-angular/src/lib/auth.service.tsprojects/auth0-angular/src/lib/interfaces.tsprojects/auth0-angular/src/public-api.ts
yogeshchoudhary147
left a comment
There was a problem hiding this comment.
The enrollment examples (Passkey, TOTP, Phone, Email, Password) use await inside switchMap callbacks that are not declared async. These will fail to compile if copied.
Fix: add async to each affected callback:
// change this
switchMap((challenge) => {
// to this
switchMap(async (challenge) => {switchMap handles Promise-returning functions natively so this works fine. Library code itself looks good.
Expose `AuthService.passkey` and `AuthService.myAccount` as Observable-based API clients, wrapping the corresponding clients from `@auth0/auth0-spa-js`. - `passkey.signup()` and `passkey.login()` handle the full WebAuthn challenge-response flow and call `authState.refresh()` on success so that `isAuthenticated$` and `user$` update automatically. - `myAccount` provides Observable wrappers for all seven MyAccount API operations: getFactors, getAuthenticationMethods, getAuthenticationMethod, deleteAuthenticationMethod, updateAuthenticationMethod, enrollmentChallenge, and enrollmentVerify. - `ObservablePasskeyApiClient` and `ObservableMyAccountApiClient` interfaces added to `interfaces.ts`, consistent with the existing `ObservableMfaApiClient` pattern. - All passkey and MyAccount error classes and types re-exported from the public API surface. - Bump `@auth0/auth0-spa-js` minimum to `^2.21.0` where these APIs were introduced. - 31 new unit tests covering delegation, return values, state side-effects, and error propagation for both API clients.
- Refactor passkey.signup/login to use of(client).pipe(concatMap, tap, catchError) pattern, aligning with getAccessTokenSilently and loginWithCustomTokenExchange: errors now emit on error$ via authState.setError(), and the Observable is lazy (deferred until subscription) - Add corresponding unit tests asserting error$ receives passkey errors - Fix switchMap callbacks in EXAMPLES.md enrollment snippets: add async to all callbacks that use await (passkey, TOTP, phone, email, password)
8610f89 to
c381541
Compare
yogeshchoudhary147
left a comment
There was a problem hiding this comment.
passkey.login() and passkey.signup() both return a TokenEndpointResponse with the access token in hand, but the implementation calls authState.refresh() instead of authState.setAccessToken(). The pattern in this codebase is:
- No token returned →
refresh()(e.g.loginWithPopup) - Token returned →
setAccessToken()(e.g.loginWithCustomTokenExchange)
Since passkey returns a token response, it should follow loginWithCustomTokenExchange:
login: (options?: PasskeyLoginOptions) =>
from(
this.auth0Client.passkey.login(options).then((tokenResponse) => {
if (tokenResponse.access_token) {
this.authState.setAccessToken(tokenResponse.access_token);
}
return tokenResponse;
})
),Both approaches update isAuthenticated$ and user$ correctly, so this is not a functional bug. But using refresh() when the token is right there signals the wrong intent and diverges from the established pattern without reason.
Passkey methods return a TokenEndpointResponse so they should follow the loginWithCustomTokenExchange pattern: call setAccessToken() with the returned token rather than calling refresh() directly.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
projects/auth0-angular/src/lib/auth.service.ts (1)
531-560: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared pipe logic for passkey signup/login.
signupandloginshare identicaltap/catchErrorlogic except for the underlying client call and options type. Consider extracting a small private helper (e.g.,handlePasskeyTokenResponse$) to avoid duplicating the access-token-setting and error-handling logic.♻️ Suggested refactor
+ private passkeyResult$( + source: Observable<TokenEndpointResponse> + ): Observable<TokenEndpointResponse> { + return source.pipe( + tap((tokenResponse) => { + if (tokenResponse.access_token) { + this.authState.setAccessToken(tokenResponse.access_token); + } + }), + catchError((error) => { + this.authState.setError(error); + this.authState.refresh(); + return throwError(error); + }) + ); + } + readonly passkey: ObservablePasskeyApiClient = { signup: (options: PasskeySignupOptions) => - of(this.auth0Client).pipe( - concatMap((client) => client.passkey.signup(options)), - tap((tokenResponse) => { - if (tokenResponse.access_token) { - this.authState.setAccessToken(tokenResponse.access_token); - } - }), - catchError((error) => { - this.authState.setError(error); - this.authState.refresh(); - return throwError(error); - }) - ), + this.passkeyResult$(from(this.auth0Client.passkey.signup(options))), login: (options?: PasskeyLoginOptions) => - of(this.auth0Client).pipe( - concatMap((client) => client.passkey.login(options)), - tap((tokenResponse) => { - if (tokenResponse.access_token) { - this.authState.setAccessToken(tokenResponse.access_token); - } - }), - catchError((error) => { - this.authState.setError(error); - this.authState.refresh(); - return throwError(error); - }) - ), + this.passkeyResult$(from(this.auth0Client.passkey.login(options))), };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@projects/auth0-angular/src/lib/auth.service.ts` around lines 531 - 560, The passkey client methods in auth.service.ts duplicate the same token handling and error handling in both signup and login. Extract the shared Observable pipeline logic from the passkey object (for example into a private helper such as handlePasskeyTokenResponse$) so it accepts the client call and options, then centralize the tap that sets authState access tokens and the catchError that sets authState error, refreshes state, and rethrows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@projects/auth0-angular/src/lib/auth.service.ts`:
- Around line 531-560: The passkey client methods in auth.service.ts duplicate
the same token handling and error handling in both signup and login. Extract the
shared Observable pipeline logic from the passkey object (for example into a
private helper such as handlePasskeyTokenResponse$) so it accepts the client
call and options, then centralize the tap that sets authState access tokens and
the catchError that sets authState error, refreshes state, and rethrows.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fd03aea7-3954-4480-8d32-71c3f58617ea
📒 Files selected for processing (7)
EXAMPLES.mdpackage.jsonprojects/auth0-angular/package.jsonprojects/auth0-angular/src/lib/auth.service.spec.tsprojects/auth0-angular/src/lib/auth.service.tsprojects/auth0-angular/src/lib/interfaces.tsprojects/auth0-angular/src/public-api.ts
✅ Files skipped from review due to trivial changes (2)
- package.json
- EXAMPLES.md
🚧 Files skipped from review as they are similar to previous changes (4)
- projects/auth0-angular/src/public-api.ts
- projects/auth0-angular/package.json
- projects/auth0-angular/src/lib/auth.service.spec.ts
- projects/auth0-angular/src/lib/interfaces.ts
Summary
This PR adds support for the Passkey and MyAccount APIs to the
auth0-angularSDK, exposing them as Observable-based clients onAuthService— consistent with how the existingmfaclient is structured.Passkey API (
AuthService.passkey)Wraps the
passkeyclient from@auth0/auth0-spa-js. Both methods handle the full WebAuthn challenge-response flow internally:passkey.signup(options)— register a new user with a passkey credentialpasskey.login(options?)— authenticate an existing user via passkey assertionBoth methods call
authState.refresh()on success so thatisAuthenticated$anduser$update automatically, consistent with other authentication flows in the SDK. On failure, errors are surfaced onerror$viaauthState.setError(), matching the pattern used bygetAccessTokenSilentlyandloginWithCustomTokenExchange.MyAccount API (
AuthService.myAccount)Wraps the
myAccountclient from@auth0/auth0-spa-js, providing Observable versions of all seven account management operations:getFactors()getAuthenticationMethods(type?)getAuthenticationMethod(id)deleteAuthenticationMethod(id)updateAuthenticationMethod(id, data)enrollmentChallenge(options)enrollmentVerify(options)Other changes
ObservablePasskeyApiClientandObservableMyAccountApiClientinterfaces added tointerfaces.ts, following theObservableMfaApiClientpatternPasskeyError,PasskeyRegisterError,PasskeyChallengeError,PasskeyGetTokenError,MyAccountApiError) and types re-exported from the public API surface@auth0/auth0-spa-jsminimum version bumped to^2.21.0where these APIs were introducedTest plan
auth.service.spec.tscovering: correct delegation to the underlying SDK, expected return values,authState.refresh()called after passkey authentication,error$receives passkey errors, and error propagation for all methodsauth.service.spec.tspass (npm test -- --testPathPattern=auth.service.spec)Manual Testing
All methods were tested end-to-end against a real Auth0 tenant using an Angular app with the local SDK build.
Passkey API
passkey.signup()— registered a new Auth0 user via WebAuthn credential creation ceremony; verified tokens returned and user created in tenantpasskey.login()— authenticated an existing passkey user via WebAuthn assertion ceremony; verified tokens returnedisAuthenticated$,user$) — verified both Observables emit updated values automatically after passkey signup and login (no manualcheckSession()call required —authState.refresh()is called internally by the SDK)MyAccount API
myAccount.getFactors()— verified MFA factors returned for authenticated usermyAccount.getAuthenticationMethods()— verified full list of enrolled authentication methods returnedmyAccount.getAuthenticationMethod(id)— verified single method retrieved by IDmyAccount.updateAuthenticationMethod(id, body)— verifiedpreferred_authentication_methodupdate for phone; confirmed expected error returned for unsupported method typesmyAccount.deleteAuthenticationMethod(id)— verified method deleted and confirmed removal via subsequentgetAuthenticationMethods()callmyAccount.enrollmentChallenge({ type: 'passkey' })— verified WebAuthn creation challenge returned with correct RP ID matching custom domainmyAccount.enrollmentVerify(...)— completed full passkey enrollment ceremony; verified new passkey method created and returned with correctrelying_party_idUsage
Summary by CodeRabbit
AuthService.passkeyObservable API for Passkey/WebAuthn signup and login.AuthService.myAccountObservable API for factors, authentication-method CRUD, and enrollment (challenge/verify).AuthServicetest suite to cover Passkey and MyAccount operations.@auth0/auth0-spa-js@^2.21.0.