Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ jobs:

test-windows-unit:
name: Run native Windows unit tests
runs-on: windows-2022
runs-on: windows-latest
environment: ${{ github.event.pull_request.head.repo.fork && 'external' || 'internal' }}

steps:
Expand Down Expand Up @@ -349,7 +349,7 @@ jobs:
${{ github.workspace }}\vcpkg\vcpkg install cpprestsdk:x64-windows openssl:x64-windows boost-system:x64-windows boost-date-time:x64-windows boost-regex:x64-windows
shell: cmd
env:
VCPKG_BINARY_SOURCES: 'clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite'
VCPKG_BINARY_SOURCES: 'clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite;x-gha,readwrite'

- name: Cache Windows example app build
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5
Expand Down
171 changes: 171 additions & 0 deletions auth0_flutter/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
- [📱 Authentication API](#-authentication-api)
- [Login with database connection](#login-with-database-connection)
- [Sign up with database connection](#sign-up-with-database-connection)
- [Log in with passkeys](#log-in-with-passkeys)
- [Sign up with passkeys](#sign-up-with-passkeys)
- [Passwordless Login](#passwordless-login)
- [Retrieve user information](#retrieve-user-information)
- [Renew credentials](#renew-credentials)
Expand All @@ -50,6 +52,7 @@
- [Listing and managing authentication methods](#listing-and-managing-authentication-methods)
- [Enrolling a factor with OTP (phone, email, TOTP)](#enrolling-a-factor-with-otp-phone-email-totp)
- [Enrolling a factor without OTP (push, recovery code)](#enrolling-a-factor-without-otp-push-recovery-code)
- [Enrolling a passkey](#enrolling-a-passkey)
- [Using DPoP](#using-dpop)
- [Errors](#errors-3)

Expand Down Expand Up @@ -1032,6 +1035,8 @@ final credentials = await auth0Web.credentials();

- [Login with database connection](#login-with-database-connection)
- [Sign up with database connection](#sign-up-with-database-connection)
- [Log in with passkeys](#log-in-with-passkeys)
- [Sign up with passkeys](#sign-up-with-passkeys)
- [Retrieve user information](#retrieve-user-information)
- [Renew credentials](#renew-credentials)
- [API client errors](#api-client-errors)
Expand Down Expand Up @@ -1100,6 +1105,134 @@ final databaseUser = await auth0.api.signup(

> 💡 You might want to log the user in after signup. See [Login with database connection](#login-with-database-connection) above for an example.

### Log in with passkeys

> This feature is available on **iOS 16.6+** and **Android 9+ (API 28)** only.

[Passkeys](https://auth0.com/docs/authenticate/database-connections/passkeys) let an existing user log in with a biometric or device PIN instead of a password, using the platform authenticator (Face ID / Touch ID on iOS, the Credential Manager on Android).

> ⚠️ Passkeys require additional configuration on both your Auth0 tenant and your app:
> - Set up a [custom domain](https://auth0.com/docs/customize/custom-domains) for your tenant. Passkeys will **not** work without one, since the relying-party domain must be a domain you own and can host the associated domain / Digital Asset Links file on.
> - Enable passkeys for your database connection and the **Passkey** grant type for your application. See [Configure passkeys](https://auth0.com/docs/authenticate/database-connections/passkeys/configure-passkeys).
> - Configure the [associated domain (iOS/macOS)](README.md#iosmacos-configure-the-associated-domain) and the equivalent [Digital Asset Links file](https://developer.android.com/identity/sign-in/credential-manager#add-support-dal) (Android) so the OS associates your app with the relying-party domain.

The SDK exposes **two** methods for passkey login — `passkeyLoginChallenge` and `passkeyCredentialExchange` — and leaves presenting the OS passkey UI to your app. The flow is:

1. Request a login challenge from Auth0 with `passkeyLoginChallenge`.
2. **In your app**, present the platform authenticator using that challenge and obtain a WebAuthn assertion. The SDK does **not** do this step — call the OS APIs directly (for example, [`ASAuthorizationController`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontroller) on iOS/macOS or [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) on Android, typically over your own platform channel), then map the result into a `PasskeyCredential`.
3. Exchange that credential for Auth0 tokens with `passkeyCredentialExchange`.

```dart
// 1. Request a login challenge from Auth0.
final challenge = await auth0.api.passkeyLoginChallenge(
connection: 'Username-Password-Authentication');

// 2. Present the OS passkey UI in your app (not provided by the SDK) using
// `challenge.authParamsPublicKey`, then build a PasskeyCredential from the
// resulting WebAuthn assertion. All values are base64url-encoded.
final credential = PasskeyCredential(
id: '<base64url credentialId>',
rawId: '<base64url credentialId>',
type: 'public-key',
authenticatorAttachment: 'platform',
response: PasskeyAuthenticatorResponse(
clientDataJSON: '<base64url clientDataJSON>',
authenticatorData: '<base64url authenticatorData>',
signature: '<base64url signature>',
userHandle: '<base64url userHandle>'));

// 3. Exchange the credential for Auth0 tokens.
final credentials = await auth0.api.passkeyCredentialExchange(
challenge: challenge,
credential: credential,
connection: 'Username-Password-Authentication');

// Store the credentials afterward
final didStore =
await auth0.credentialsManager.storeCredentials(credentials);
```

<details>
<summary>Add an audience and scope values</summary>

```dart
final credentials = await auth0.api.passkeyCredentialExchange(
challenge: challenge,
credential: credential,
connection: 'Username-Password-Authentication',
audience: 'YOUR_AUTH0_API_IDENTIFIER',
scopes: {'profile', 'email', 'offline_access', 'read:todos'});
```

</details>

### Sign up with passkeys

> This feature is available on **iOS 16.6+** and **Android 9+ (API 28)** only.

[Passkeys](https://auth0.com/docs/authenticate/database-connections/passkeys) let users register with a biometric or device PIN instead of a password, using the platform authenticator (Face ID / Touch ID on iOS, the Credential Manager on Android).

> ⚠️ Passkeys require additional configuration on both your Auth0 tenant and your app:
> - Set up a [custom domain](https://auth0.com/docs/customize/custom-domains) for your tenant. Passkeys will **not** work without one, since the relying-party domain must be a domain you own and can host the associated domain / Digital Asset Links file on.
> - Enable passkeys for your database connection and the **Passkey** grant type for your application. See [Configure passkeys](https://auth0.com/docs/authenticate/database-connections/passkeys/configure-passkeys).
> - Configure the [associated domain (iOS/macOS)](README.md#iosmacos-configure-the-associated-domain) and the equivalent [Digital Asset Links file](https://developer.android.com/identity/sign-in/credential-manager#add-support-dal) (Android) so the OS associates your app with the relying-party domain.

The SDK exposes **two** methods for passkey signup — `passkeySignupChallenge` and `passkeyCredentialExchange` — and leaves presenting the OS passkey UI to your app. The flow is:

1. Request a registration challenge from Auth0 with `passkeySignupChallenge`.
2. **In your app**, present the platform authenticator using that challenge and obtain a WebAuthn attestation. The SDK does **not** do this step — call the OS APIs directly (for example, [`ASAuthorizationController`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontroller) on iOS/macOS or [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) on Android, typically over your own platform channel), then map the result into a `PasskeyCredential`.
3. Exchange that credential for Auth0 tokens with `passkeyCredentialExchange` — the same method used for login.

You can identify the new user with any combination of `email`, `phoneNumber`, `username`, `name`, `givenName`, `familyName`, `nickname`, and `picture`, depending on how your connection is configured.

```dart
// 1. Request a registration challenge from Auth0. You can identify the new
// user with any combination of email, phoneNumber, username, name,
// givenName, familyName, nickname, and picture.
final challenge = await auth0.api.passkeySignupChallenge(
email: 'jane.smith@example.com',
name: 'Jane Smith',
givenName: 'Jane',
familyName: 'Smith',
connection: 'Username-Password-Authentication');

// 2. Present the OS passkey-creation UI in your app (not provided by the SDK)
// using `challenge.authParamsPublicKey`, then build a PasskeyCredential from
// the resulting WebAuthn attestation. All values are base64url-encoded.
final credential = PasskeyCredential(
id: '<base64url credentialId>',
rawId: '<base64url credentialId>',
type: 'public-key',
authenticatorAttachment: 'platform',
response: PasskeyAuthenticatorResponse(
clientDataJSON: '<base64url clientDataJSON>',
attestationObject: '<base64url attestationObject>'));

// 3. Exchange the credential for Auth0 tokens.
final credentials = await auth0.api.passkeyCredentialExchange(
challenge: challenge,
credential: credential,
connection: 'Username-Password-Authentication');

// Store the credentials afterward
final didStore =
await auth0.credentialsManager.storeCredentials(credentials);
```

<details>
<summary>Add an audience and scope values</summary>

```dart
final credentials = await auth0.api.passkeyCredentialExchange(
challenge: challenge,
credential: credential,
connection: 'Username-Password-Authentication',
audience: 'YOUR_AUTH0_API_IDENTIFIER',
scopes: {'profile', 'email', 'offline_access', 'read:todos'});
```

</details>

### Passwordless Login
Passwordless is a two-step authentication flow that requires the **Passwordless OTP** grant to be enabled for your Auth0 application. Check [our documentation](https://auth0.com/docs/get-started/applications/application-grant-types) for more information.

Expand Down Expand Up @@ -1445,6 +1578,44 @@ final method = await myAccount.confirmEnrollment(

The same flow applies to `enrollRecoveryCode` (`factorType: 'recovery-code'`).

### Enrolling a passkey

A signed-in user can add a passkey as a new authentication method. Like passkey login and signup, this is a two-step flow and the SDK leaves presenting the OS passkey UI to your app:

1. Request an enrollment challenge with `enrollPasskeyChallenge`.
2. **In your app**, present the platform authenticator using `challenge.authParamsPublicKey` to create a passkey, and map the resulting WebAuthn attestation into a `PasskeyCredential`. The SDK does **not** do this step — call the OS APIs directly (for example, [`ASAuthorizationController`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontroller) on iOS/macOS or [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) on Android, typically over your own platform channel).
3. Submit the credential with `enrollPasskey` to complete the enrollment.

> ⚠️ Passkeys require a [custom domain](https://auth0.com/docs/customize/custom-domains) on your tenant and additional configuration. See [Sign up with passkeys](#sign-up-with-passkeys) for details.

The access token must include the `create:me:authentication_methods` scope.

```dart
// 1. Request an enrollment challenge.
final challenge = await myAccount.enrollPasskeyChallenge();

// 2. Present the OS passkey creation UI in your app (not provided by the SDK)
// using `challenge.authParamsPublicKey`, then build a PasskeyCredential from
// the resulting WebAuthn attestation.
final credential = PasskeyCredential(
id: '...',
rawId: '...',
type: 'public-key',
response: PasskeyAuthenticatorResponse(
clientDataJSON: '...',
attestationObject: '...',
),
);

// 3. Submit the credential to complete the enrollment.
final method = await myAccount.enrollPasskey(
challenge: challenge,
credential: credential,
);

print('Enrolled passkey: ${method.id} (${method.relyingPartyId})');
```

### Using DPoP

To secure My Account API requests with [DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof-of-Possession) sender-constrained tokens, set `useDPoP` to `true` when creating the client. It defaults to `false`. The DPoP key pair is generated and stored securely on the device (Keychain on iOS, Keystore on Android).
Expand Down
35 changes: 35 additions & 0 deletions auth0_flutter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,8 @@ void dispose() {
- [Check for stored credentials](EXAMPLES.md#check-for-stored-credentials) - check if the user is already logged in when your app starts up.
- [Retrieve stored credentials](EXAMPLES.md#retrieve-stored-credentials) - fetch the user's credentials from the storage, automatically renewing them if they have expired.
- [Retrieve user information](EXAMPLES.md#retrieve-user-information) - fetch the latest user information from the `/userinfo` endpoint.
- [Log in with passkeys](EXAMPLES.md#log-in-with-passkeys) - authenticate an existing user with a passkey using the platform authenticator (iOS/Android only).
- [Sign up with passkeys](EXAMPLES.md#sign-up-with-passkeys) - register a new user with a passkey using the platform authenticator (iOS/Android only).
- [Native to Web SSO](EXAMPLES.md#native-to-web-sso) - obtain a session transfer token to authenticate a WebView without re-prompting the user.
- [Handle Android process death](#android-handle-process-death-during-login) - recover credentials when the OS kills your app during login.

Expand Down Expand Up @@ -621,6 +623,9 @@ void dispose() {
- [resetPassword](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/resetPassword.html)
- [signup](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/signup.html)
- [userProfile](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/userProfile.html)
- [passkeyLoginChallenge](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/passkeyLoginChallenge.html) - request a WebAuthn assertion challenge to log in an existing user with a passkey (iOS 16.6+ / Android 9+)
- [passkeySignupChallenge](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/passkeySignupChallenge.html) - request a WebAuthn attestation challenge to register a new user with a passkey (iOS 16.6+ / Android 9+)
- [passkeyCredentialExchange](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/passkeyCredentialExchange.html) - exchange a passkey credential (assertion or attestation) for Auth0 tokens

#### Credentials Manager

Expand Down Expand Up @@ -715,6 +720,36 @@ await myAccount.confirmEnrollment(
);
```

##### Passkey enrollment

A signed-in user can add a passkey as a new authentication method. Like passkey login and signup, the SDK handles only the Auth0 API calls — presenting the OS passkey UI is left to your app:

```dart
// 1. Request an enrollment challenge.
final challenge = await myAccount.enrollPasskeyChallenge();

// 2. Present the OS passkey-creation UI in your app (not provided by the SDK)
// using `challenge.authParamsPublicKey`, then build a PasskeyCredential from
// the resulting WebAuthn attestation.
final credential = PasskeyCredential(
id: '...',
rawId: '...',
type: 'public-key',
response: PasskeyAuthenticatorResponse(
clientDataJSON: '...',
attestationObject: '...',
),
);

// 3. Submit the credential to complete enrollment.
final method = await myAccount.enrollPasskey(
challenge: challenge,
credential: credential,
);
```

The access token must include the `create:me:authentication_methods` scope. See [Enrolling a passkey](EXAMPLES.md#enrolling-a-passkey) for the full example.

##### DPoP

To secure My Account API requests with [DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof-of-Possession) sender-constrained tokens, set `useDPoP` to `true` when creating the client. It defaults to `false`. The DPoP key pair is generated and stored securely on the device (Keychain on iOS, Keystore on Android).
Expand Down
1 change: 1 addition & 0 deletions auth0_flutter/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'com.auth0.android:auth0:3.18.0'
implementation 'com.google.code.gson:gson:2.10.1'
Comment thread
NandanPrabhu marked this conversation as resolved.
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0'
testImplementation "org.mockito.kotlin:mockito-kotlin:4.1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ class Auth0FlutterAuthMethodCallHandler(
private val apiRequestHandlers: List<ApiRequestHandler>
) : MethodCallHandler {
lateinit var context: Context

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
val request = MethodCallRequest.fromCall(call)

val apiHandler = apiRequestHandlers.find { it.method == call.method }
if (apiHandler != null) {
val api = AuthenticationAPIClient(request.account)

val useDPoP = request.data["useDPoP"] as? Boolean ?: false
if (useDPoP) {
api.useDPoP(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
GetAuthenticationMethodRequestHandler(),
DeleteAuthenticationMethodRequestHandler(),
GetFactorsRequestHandler(),
EnrollPasskeyChallengeRequestHandler(),
EnrollPasskeyRequestHandler(),
EnrollPhoneRequestHandler(),
EnrollEmailRequestHandler(),
EnrollTotpRequestHandler(),
Expand Down Expand Up @@ -133,7 +135,10 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
RenewApiRequestHandler(),
CustomTokenExchangeApiRequestHandler(),
SSOExchangeApiRequestHandler(),
ResetPasswordApiRequestHandler()
ResetPasswordApiRequestHandler(),
PasskeyLoginChallengeApiRequestHandler(),
PasskeySignupChallengeApiRequestHandler(),
PasskeyCredentialExchangeApiRequestHandler()
)
)
authCallHandler.context = context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result

class Auth0FlutterWebAuthMethodCallHandler(private val requestHandlers: List<WebAuthRequestHandler>) : MethodCallHandler {
lateinit var activity: Activity
lateinit var activity: Activity

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
val requestHandler = requestHandlers.find { it.method == call.method }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ private object BiometricAuthLevel {
}

class CredentialsManagerMethodCallHandler(private val requestHandlers: List<CredentialsManagerRequestHandler>) : MethodCallHandler {
lateinit var activity: Activity
// Null while the plugin is detached from an Activity (see ActivityAware
// callbacks in Auth0FlutterPlugin). Only required for biometric/local
// authentication, which needs a FragmentActivity.
var activity: Activity? = null
lateinit var context: Context

private data class ManagerCacheKey(
Expand Down
Loading
Loading