Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions DISCOVERY_MY_ACCOUNT_API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# My Account API - Flutter SDK Discovery Document

**Date:** 2026-05-12
**Status:** Discovery / Pre-Implementation
**Author:** Utkrisht Sahu

---

## 1. Overview

The My Account API (`/me/v1/`) enables end-users to self-manage their authentication methods (MFA factors) without requiring Management API tokens or admin intervention. This document outlines the discovery findings and proposed approach for implementing My Account API support in the auth0-flutter SDK.

---

## 2. What is the My Account API?

The My Account API allows authenticated users to:

- **List** their enrolled authentication methods
- **Get** details of a specific authentication method
- **Update** an authentication method (e.g., change phone preferences)
- **Delete** an authentication method
- **Enroll** new authentication methods (phone, email, TOTP, push notifications, passkeys, recovery codes)
- **Verify** enrollments via OTP
- **View** enrollable factors available for the tenant

### Authentication

- Uses OAuth 2.0 access tokens with audience: `https://{domain}/me/`
- Requires granular scopes:
- `read:me:authentication_methods`
- `create:me:authentication_methods`
- `update:me:authentication_methods`
- `delete:me:authentication_methods`
- `read:me:factors`

### Security

- Enforces step-up authentication (2FA within 15 minutes by default)
- Rate limited at 25 requests/second per tenant
- Machine-to-machine access is prohibited

---

## 3. Existing Native SDK Implementations

### 3.1 Android (`Auth0.Android`)

**Class:** `MyAccountAPIClient`

| Method | Description |
|--------|-------------|
| `getAuthenticationMethods()` | List all enrolled methods |
| `getAuthenticationMethodById(id)` | Get specific method |
| `updateAuthenticationMethodById(id, ...)` | Update method name/preferences |
| `deleteAuthenticationMethod(id)` | Delete a method |
| `enrollPhone(phoneNumber, type)` | Enroll phone (SMS/Voice) |
| `enrollEmail(emailAddress)` | Enroll email |
| `enrollTotp()` | Enroll authenticator app |
| `enrollPushNotification()` | Enroll push notifications |
| `enrollRecoveryCode()` | Generate recovery codes |
| `passkeyEnrollmentChallenge(...)` | Get passkey challenge |
| `enroll(passkey)` | Complete passkey enrollment |
| `verifyOtp(id, authSession, otp)` | Verify OTP for enrollment |
| `verify(id, authSession)` | Confirm non-OTP enrollment |
| `getFactors()` | List enrollable factors |

- Returns `Request<T, MyAccountException>` objects
- Access token provided at client construction
- `MyAccountException` includes: `code`, `statusCode`, `description`, `validationErrors`

### 3.2 Swift (`Auth0.swift`)

**Classes:** `Auth0MyAccount` + `Auth0MyAccountAuthenticationMethods`

| Method | Description |
|--------|-------------|
| `getAuthenticationMethods()` | List all enrolled methods |
| `getAuthenticationMethod(by: id)` | Get specific method |
| `deleteAuthenticationMethod(by: id)` | Delete a method |
| `enrollPhone(phoneNumber:, preferredAuthenticationMethod:)` | Enroll phone |
| `confirmPhoneEnrollment(id:, authSession:, otpCode:)` | Confirm phone |
| `enrollEmail(emailAddress:)` | Enroll email |
| `confirmEmailEnrollment(id:, authSession:, otpCode:)` | Confirm email |
| `enrollTOTP()` | Enroll authenticator app |
| `confirmTOTPEnrollment(id:, authSession:, otpCode:)` | Confirm TOTP |
| `enrollPushNotification()` | Enroll push |
| `confirmPushNotificationEnrollment(id:, authSession:)` | Confirm push |
| `enrollRecoveryCode()` | Generate recovery codes |
| `confirmRecoveryCodeEnrollment(id:, authSession:)` | Confirm recovery |
| `passkeyEnrollmentChallenge(...)` | Get passkey challenge |
| `enroll(passkey:, challenge:)` | Complete passkey enrollment |
| `getFactors()` | List enrollable factors |

- Protocol-based architecture
- Factory: `Auth0.myAccount(token: accessToken, domain: "...")`
- URL pattern: `{domain}/me/v1/authentication-methods`
- Two-step enrollment: challenge then confirm
- `MyAccountError` includes: `code`, `statusCode`, `title`, `detail`, `validationErrors`

### 3.3 SPA (`auth0-spa-js`)

**Does NOT implement My Account Authentication Methods.**

The SPA SDK only implements Connected Accounts (Token Vault), which is a separate feature for linking external provider tokens β€” not related to My Account MFA management.

---

## 4. Platform Support Matrix

| Feature | Android | iOS/macOS (Swift) | Web (SPA) | Flutter (Proposed) |
|---------|:-------:|:-----------------:|:---------:|:------------------:|
| List authentication methods | βœ… | βœ… | ❌ | βœ… Mobile only |
| Get authentication method | βœ… | βœ… | ❌ | βœ… Mobile only |
| Update authentication method | βœ… | ❌ | ❌ | βœ… Android only |
| Delete authentication method | βœ… | βœ… | ❌ | βœ… Mobile only |
| Enroll phone (SMS/Voice) | βœ… | βœ… | ❌ | βœ… Mobile only |
| Enroll email | βœ… | βœ… | ❌ | βœ… Mobile only |
| Enroll TOTP | βœ… | βœ… | ❌ | βœ… Mobile only |
| Enroll push notifications | βœ… | βœ… | ❌ | βœ… Mobile only |
| Enroll recovery codes | βœ… | βœ… | ❌ | βœ… Mobile only |
| Passkey enrollment | βœ… | βœ… (iOS 16.6+) | ❌ | βœ… Mobile only |
| Verify OTP | βœ… | βœ… | ❌ | βœ… Mobile only |
| Get factors | βœ… | βœ… | ❌ | βœ… Mobile only |
| **Web support** | β€” | β€” | ❌ | **❌ Not supported** |

---

## 5. Flutter Implementation Strategy

### 5.1 Architecture

Flutter will act as a **bridge layer** β€” delegating to the native SDKs (Auth0.Android and Auth0.swift) via method channels. No direct HTTP calls to the My Account API.

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ User Application β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ auth0_flutter (Public Dart API) β”‚
β”‚ MyAccountApi class β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ auth0_flutter_platform_interface β”‚
β”‚ Auth0FlutterMyAccountPlatform (abstract) β”‚
β”‚ MethodChannelAuth0FlutterMyAccount β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Method Channel Bridge β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Android (Kotlin) β”‚ iOS/macOS (Swift) β”‚
β”‚ MyAccountAPIClient β”‚ Auth0MyAccount β”‚
β”‚ (Auth0.Android SDK) β”‚ (Auth0.swift SDK) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

### 5.2 Public API Design

```dart
// Access My Account API
final myAccount = auth0.myAccount(accessToken: token);

// List methods
final methods = await myAccount.getAuthenticationMethods();

// Get specific method
final method = await myAccount.getAuthenticationMethod(id: 'auth_method_id');

// Delete method
await myAccount.deleteAuthenticationMethod(id: 'auth_method_id');

// Enroll phone
final challenge = await myAccount.enrollPhone(
phoneNumber: '+1234567890',
type: PhoneType.sms,
);

// Verify enrollment
await myAccount.verifyOtp(
id: challenge.id,
authSession: challenge.authSession,
otp: '123456',
);

// Get available factors
final factors = await myAccount.getFactors();
```

### 5.3 Key Models

- `AuthenticationMethod` β€” represents an enrolled MFA method
- `Factor` β€” represents an enrollable factor type
- `EnrollmentChallenge` β€” returned from enrollment methods (contains `id`, `authSession`)
- `MyAccountException` β€” error with `code`, `statusCode`, `description`, `validationErrors`
- `PhoneType` β€” enum: `sms`, `voice`

### 5.4 Web Platform

**Not supported.** The SPA SDK does not implement My Account Authentication Methods. Calling My Account methods on web will throw `UnsupportedError`.

---

## 6. Phased Rollout Plan

### Phase 1: Core Read/Delete Operations
- `getAuthenticationMethods()`
- `getAuthenticationMethod(id:)`
- `deleteAuthenticationMethod(id:)`
- `getFactors()`
- `MyAccountException` error handling
- Platform interface + method channel setup

### Phase 2: Enrollments + Verification
- `enrollPhone(phoneNumber:, type:)`
- `enrollEmail(emailAddress:)`
- `enrollTotp()`
- `enrollPushNotification()`
- `enrollRecoveryCode()`
- `verifyOtp(id:, authSession:, otp:)`
- `verify(id:, authSession:)`

### Phase 3: Passkey Enrollment
- `passkeyEnrollmentChallenge(userIdentityId:, connection:)`
- `enrollPasskey(passkey:, challenge:)`
- Platform version guards (iOS 16.6+, Android API level checks)

---

## 7. Open Questions

1. Do we need `updateAuthenticationMethod()` on iOS? (Swift SDK doesn't expose it, Android does)
2. Should we throw `UnsupportedError` on web, or silently degrade?
3. What minimum iOS/Android versions should we target for passkey support?
4. Should the access token be passed per-call or at `MyAccountApi` construction time?

---

## 8. References

- [My Account API Documentation](https://auth0.com/docs/manage-users/my-account-api)
- [Auth0.Android SDK](https://github.com/auth0/Auth0.Android)
- [Auth0.swift SDK](https://github.com/auth0/Auth0.swift)
- [auth0-spa-js SDK](https://github.com/auth0/auth0-spa-js)
120 changes: 120 additions & 0 deletions PR_835_REVIEW_RESPONSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# PR #835 Review Response: My Account API

## Summary

Consolidated review of both internal and external feedback. Categorized into: **Fixed**, **Acknowledged (Won't Fix)**, and **Deferred**.

---

## Fixed

### 1. Android: `totp_uri` mapped to wrong value

**File:** `auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt`

**Issue:** Both `totp_secret` and `totp_uri` were set to `challenge.manualInputCode`. The `totp_uri` should be `challenge.barcodeUri` (the `otpauth://` URI for QR code scanning).

**Fix applied:**
```kotlin
// Before
put("totp_uri", challenge.manualInputCode)

// After
put("totp_uri", challenge.barcodeUri)
```

This now matches the iOS implementation which correctly uses `authenticatorQRCodeURI` for `totp_uri`.

---

## Acknowledged - By Design (Won't Fix)

### 2. iOS `verifyOtp` uses `confirmPhoneEnrollment`

**Context:** The Auth0.swift SDK has separate confirm methods per factor type (`confirmPhoneEnrollment`, `confirmEmailEnrollment`, `confirmTOTPEnrollment`). However, the OTP confirmation logic is identical across these methods at the API level β€” they all POST an OTP code with an auth session to the same endpoint pattern. The `confirmPhoneEnrollment` method works generically for all OTP-based confirmations.

**Decision:** No change. The Android SDK exposes a single generic `verifyOtp(id, otp, authSession)` method which confirms this is the intended API design. If Auth0.swift separates the confirm calls in a future version in a breaking way, we'll update then.

### 3. `verifyOtp` returns `void` (discards `AuthenticationMethod` on Android)

**Context:** The Dart API is designed as `Future<void>` for `verifyOtp`. The caller already has the enrollment ID and can call `getAuthenticationMethod(id)` to get the confirmed method if needed. This matches the pattern where enrollment + verify is a two-step flow and the caller tracks the ID themselves.

**Decision:** No change. This is intentional API design β€” keeping `verifyOtp` as a void confirmation action rather than a data-fetching operation.

### 4. iOS `AuthenticationMethod.asDictionary()` β€” `email`, `totp_secret`, `totp_uri` are nil

**Context:** The Auth0.swift `AuthenticationMethod` struct is a flat type β€” it does not have subclasses for email/TOTP like Android's polymorphic `EmailAuthenticationMethod`/`TotpAuthenticationMethod`. The Swift SDK's base `AuthenticationMethod` only exposes `phoneNumber` and `name` as type-specific fields. Email and TOTP details are only available during enrollment (via `EnrollmentChallenge` types, which ARE correctly mapped).

**Decision:** No change. This is a limitation of the Auth0.swift SDK's type model. The relevant fields are returned during enrollment when they matter. For `getAuthenticationMethods()`, the `type` field identifies the method type and `id` can be used for management operations.

### 5. `Factor` model uses `name` + `enabled` instead of `type` + `usage`

**Context:** The native SDK `Factor` has `type` (String) and `usage` (array of strings like `["primary", "secondary"]`). The Flutter model simplifies this to `name` (mapped from `type`) and `enabled` (hardcoded `true`). The rationale: if a factor is returned by the API, it IS enabled β€” disabled factors are not returned. The `usage` array is not actionable for self-service MFA management.

**Decision:** No change for v1. The simplified model serves the primary use case (showing available factors for enrollment). If users need `usage` data, we can add it in a minor version bump without breaking changes.

### 6. iOS `isNetworkError` hardcoded to `false`

**Context:** The Auth0.swift `MyAccountError` type does not expose a network error flag directly. The error is an API response error (with status code and error code). Network-level failures (no connectivity, timeout) would surface as a different error type entirely and would not reach this code path.

**Decision:** No change. The `isNetworkError: false` is correct for API errors. True network errors are handled at the URLSession/connectivity layer.

### 7. iOS `userAgent` not applied to client

**Context:** The Auth0.swift `myAccount()` factory creates a client with built-in telemetry. The `using()` pattern for custom user agents is not available on `MyAccount` in the current Auth0.swift version. The telemetry parameter is accepted in the handler for future use when the Swift SDK supports it.

**Decision:** No change now. Will be wired up when Auth0.swift adds telemetry customization to MyAccount.

---

## Deferred (Future Enhancement)

### 8. Missing `updateAuthenticationMethod`

The native SDKs support updating an authentication method's name or preferred delivery mechanism. This is a valid feature gap but out of scope for the initial release.

**Tracked for:** v2 enhancement

### 9. Missing passkey enrollment

Both native SDKs support passkey enrollment via WebAuthn/FIDO2. This requires significant platform-specific work (platform credential APIs, attestation handling) and is deferred.

**Tracked for:** Future release (depends on Flutter passkey ecosystem maturity)

### 10. Per-factor confirmation methods (push/recovery code without OTP)

Push notification and recovery code enrollments don't require OTP verification. The current `verifyOtp` method won't work for these. However, the enrollment responses for push and recovery code return all needed data immediately β€” push enrollment auto-confirms, and recovery code enrollment returns the code directly.

**Decision:** Deferred. If auto-confirmation doesn't cover these cases in practice, we'll add `confirmPushEnrollment` and `confirmRecoveryCodeEnrollment` methods.

### 11. Missing `confirmed` field on Dart `AuthenticationMethod`

The Android side serializes `confirmed` but the Dart model doesn't include it. This is useful for showing enrollment status in UI.

**Tracked for:** Next minor version (additive, non-breaking)

### 12. Dart-level unit tests

No Dart tests for `MethodChannelAuth0FlutterMyAccount`, `MyAccountApi`, or model parsing. Android and iOS have native tests. Dart tests would improve confidence in serialization layer.

**Tracked for:** Follow-up PR

### 13. Android empty string fallback for missing `accessToken`

```kotlin
val accessToken = request.data["accessToken"] as? String ?: ""
```

Should fail fast instead of making API calls with empty bearer token. Low risk since the Dart layer always provides the token, but defensive programming suggests erroring early.

**Tracked for:** Follow-up hardening PR

---

## Changes Made in This Response

| # | File | Change |
|---|------|--------|
| 1 | `MyAccountExtensions.kt:52` | Fixed `totp_uri` to use `challenge.barcodeUri` instead of `challenge.manualInputCode` |

---
Loading
Loading