Skip to content
Draft
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
2 changes: 2 additions & 0 deletions infra-gen2/backends/auth/webauthn/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
amplify_outputs.dart
.amplify
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PreSignUpTriggerHandler } from "aws-lambda";
import { preSignUpTriggerHandler } from "infra-common";

export const handler: PreSignUpTriggerHandler = preSignUpTriggerHandler;
28 changes: 28 additions & 0 deletions infra-gen2/backends/auth/webauthn/amplify/auth/resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { defineAuth, defineFunction } from "@aws-amplify/backend";

export const preSignUp = defineFunction({
name: "pre-sign-up",
entry: "./pre-sign-up-handler.ts",
});

export const auth = defineAuth({
loginWith: {
email: { otpLogin: true },
phone: { otpLogin: true },
webAuthn: true, // relyingPartyId auto-resolves to localhost in sandbox
},
passwordlessOptions: {
preferredChallenge: "WEB_AUTHN"
},
userAttributes: {
phoneNumber: {
required: false
}
},
triggers: {
preSignUp,
},
});
9 changes: 9 additions & 0 deletions infra-gen2/backends/auth/webauthn/amplify/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";

defineBackend({
auth,
});
1 change: 1 addition & 0 deletions infra-gen2/backends/auth/webauthn/amplify/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "type": "module" }
17 changes: 17 additions & 0 deletions infra-gen2/backends/auth/webauthn/amplify/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"$amplify/*": [
"../.amplify/generated/*"
]
}
}
}
5 changes: 5 additions & 0 deletions infra-gen2/backends/auth/webauthn/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "webauthn",
"version": "1.0.0",
"main": "index.js"
}
7 changes: 7 additions & 0 deletions infra-gen2/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions infra-gen2/tool/deploy_gen2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ const List<AmplifyBackendGroup> infraConfig = [
identifier: 'user-login-mfa',
pathToSource: 'infra-gen2/backends/auth/username-login-mfa',
),
AmplifyBackend(
name: 'webauthn',
identifier: 'webauthn',
pathToSource: 'infra-gen2/backends/auth/webauthn',
),
],
),
AmplifyBackendGroup(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1466,4 +1466,55 @@ class AuthCategory extends AmplifyCategory<AuthPluginInterface> {
AuthCategoryMethod.deleteUser,
() => defaultPlugin.deleteUser(),
);

/// {@template amplify_core.amplify_auth_category.associate_webauthn_credential}
/// Registers a new passkey/WebAuthn credential on the user's account.
///
/// This orchestrates the full registration flow: requests creation options
/// from the server, triggers the platform WebAuthn ceremony, and sends
/// the credential back to the server.
///
/// Requires an authenticated user session.
/// {@endtemplate}
Future<void> associateWebAuthnCredential() => identifyCall(
AuthCategoryMethod.associateWebAuthnCredential,
() => defaultPlugin.associateWebAuthnCredential(),
);

/// {@template amplify_core.amplify_auth_category.list_webauthn_credentials}
/// Lists all passkey/WebAuthn credentials registered on the user's account.
///
/// Returns a list of [AuthWebAuthnCredential] objects representing each
/// registered credential.
///
/// Requires an authenticated user session.
/// {@endtemplate}
Future<List<AuthWebAuthnCredential>> listWebAuthnCredentials() =>
identifyCall(
AuthCategoryMethod.listWebAuthnCredentials,
() => defaultPlugin.listWebAuthnCredentials(),
);

/// {@template amplify_core.amplify_auth_category.delete_webauthn_credential}
/// Deletes a specific passkey/WebAuthn credential from the user's account.
///
/// The [credentialId] parameter identifies which credential to delete.
///
/// Requires an authenticated user session.
/// {@endtemplate}
Future<void> deleteWebAuthnCredential(String credentialId) => identifyCall(
AuthCategoryMethod.deleteWebAuthnCredential,
() => defaultPlugin.deleteWebAuthnCredential(credentialId),
);

/// {@template amplify_core.amplify_auth_category.is_passkey_supported}
/// Checks if passkey/WebAuthn is supported on the current platform.
///
/// Returns `true` if the platform supports WebAuthn credentials,
/// `false` otherwise.
/// {@endtemplate}
Future<bool> isPasskeySupported() => identifyCall(
AuthCategoryMethod.isPasskeySupported,
() => defaultPlugin.isPasskeySupported(),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'package:amplify_core/amplify_core.dart';
import 'package:amplify_core/src/config/amplify_outputs/auth/mfa.dart';
import 'package:amplify_core/src/config/amplify_outputs/auth/oauth_outputs.dart';
import 'package:amplify_core/src/config/amplify_outputs/auth/password_policy.dart';
import 'package:amplify_core/src/config/amplify_outputs/auth/passwordless_outputs.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';

part 'auth_outputs.g.dart';
Expand All @@ -31,10 +33,34 @@ class AuthOutputs
this.unauthenticatedIdentitiesEnabled = true,
this.mfaConfiguration,
this.mfaMethods,
this.passwordless,
});

factory AuthOutputs.fromJson(Map<String, Object?> json) =>
_$AuthOutputsFromJson(json);
factory AuthOutputs.fromJson(Map<String, Object?> json) {
final base = _$AuthOutputsFromJson(json);
final passwordlessJson = json['passwordless'];
return AuthOutputs(
awsRegion: base.awsRegion,
userPoolId: base.userPoolId,
userPoolClientId: base.userPoolClientId,
userPoolEndpoint: base.userPoolEndpoint,
appClientSecret: base.appClientSecret,
identityPoolId: base.identityPoolId,
passwordPolicy: base.passwordPolicy,
oauth: base.oauth,
standardRequiredAttributes: base.standardRequiredAttributes,
usernameAttributes: base.usernameAttributes,
userVerificationTypes: base.userVerificationTypes,
unauthenticatedIdentitiesEnabled: base.unauthenticatedIdentitiesEnabled,
mfaConfiguration: base.mfaConfiguration,
mfaMethods: base.mfaMethods,
passwordless: passwordlessJson == null
? null
: PasswordlessOutputs.fromJson(
passwordlessJson as Map<String, Object?>,
),
);
}

/// The AWS region of Amazon Cognito resources.
final String awsRegion;
Expand Down Expand Up @@ -83,6 +109,10 @@ class AuthOutputs
/// {@macro amplify_core.amplify_outputs.maf_method}
final List<MfaMethod>? mfaMethods;

/// Passwordless authentication configuration.
@JsonKey(includeFromJson: false, includeToJson: false)
final PasswordlessOutputs? passwordless;

@override
List<Object?> get props => [
awsRegion,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'package:amplify_core/amplify_core.dart';

/// Passwordless authentication configuration from Amplify outputs.
class PasswordlessOutputs {
const PasswordlessOutputs({
this.emailOtpEnabled = false,
this.smsOtpEnabled = false,
this.webAuthnEnabled = false,
this.preferredChallenge,
});

factory PasswordlessOutputs.fromJson(Map<String, Object?> json) {
return PasswordlessOutputs(
emailOtpEnabled: json['email_otp_enabled'] as bool? ?? false,
smsOtpEnabled: json['sms_otp_enabled'] as bool? ?? false,
webAuthnEnabled: json['web_authn'] != null,
preferredChallenge: _parseChallenge(
json['preferred_challenge'] as String?,
),
);
}

static AuthFactorType? _parseChallenge(String? value) {
switch (value) {
case 'WEB_AUTHN':
return AuthFactorType.webAuthn;
case 'EMAIL_OTP':
return AuthFactorType.emailOtp;
case 'SMS_OTP':
return AuthFactorType.smsOtp;
case 'PASSWORD':
case 'PASSWORD_SRP':
return AuthFactorType.password;
default:
return null;
}
}

final bool emailOtpEnabled;
final bool smsOtpEnabled;
final bool webAuthnEnabled;

/// The preferred first-factor challenge from the backend config.
final AuthFactorType? preferredChallenge;

/// Returns all enabled passwordless methods.
List<AuthFactorType> get enabledMethods => [
if (webAuthnEnabled) AuthFactorType.webAuthn,
if (emailOtpEnabled) AuthFactorType.emailOtp,
if (smsOtpEnabled) AuthFactorType.smsOtp,
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ enum AuthCategoryMethod with AmplifyCategoryMethod {
getMfaPreference('50'),
setUpTotp('51'),
verifyTotpSetup('52'),
fetchCurrentDevice('59');
fetchCurrentDevice('59'),
associateWebAuthnCredential('60'),
listWebAuthnCredentials('61'),
deleteWebAuthnCredential('62'),
isPasskeySupported('63');

const AuthCategoryMethod(this.method);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,30 @@ abstract class AuthPluginInterface extends AmplifyPluginInterface {
Future<void> deleteUser() {
throw UnimplementedError('deleteUser() has not been implemented.');
}

/// {@macro amplify_core.amplify_auth_category.associate_webauthn_credential}
Future<void> associateWebAuthnCredential() {
throw UnimplementedError(
'associateWebAuthnCredential() has not been implemented.',
);
}

/// {@macro amplify_core.amplify_auth_category.list_webauthn_credentials}
Future<List<AuthWebAuthnCredential>> listWebAuthnCredentials() {
throw UnimplementedError(
'listWebAuthnCredentials() has not been implemented.',
);
}

/// {@macro amplify_core.amplify_auth_category.delete_webauthn_credential}
Future<void> deleteWebAuthnCredential(String credentialId) {
throw UnimplementedError(
'deleteWebAuthnCredential() has not been implemented.',
);
}

/// {@macro amplify_core.amplify_auth_category.is_passkey_supported}
Future<bool> isPasskeySupported() {
throw UnimplementedError('isPasskeySupported() has not been implemented.');
}
}
10 changes: 9 additions & 1 deletion packages/amplify_core/lib/src/types/auth/auth_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ export '../exception/amplify_exception.dart'
UserCancelledException,
AuthValidationException,
NetworkException,
UnknownException;
UnknownException,
PasskeyException,
PasskeyNotSupportedException,
PasskeyCancelledException,
PasskeyRegistrationFailedException,
PasskeyAssertionFailedException,
PasskeyRpMismatchException;
// Attributes
export 'attribute/auth_next_update_attribute_step.dart';
export 'attribute/auth_update_attribute_step.dart';
Expand All @@ -34,6 +40,8 @@ export 'auth_device.dart';
export 'auth_next_step.dart';
// Auto Sign In
export 'auto_sign_in/auto_sign_in_options.dart';
// Credentials
export 'credential/auth_webauthn_credential.dart';
// Hub
export 'hub/auth_hub_event.dart';
// MFA
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'package:aws_common/aws_common.dart';
import 'package:meta/meta.dart';

/// {@category Auth}
/// {@template amplify_core.auth_webauthn_credential}
/// Common interface for WebAuthn/passkey credentials registered with an authentication provider.
/// {@endtemplate}
@immutable
abstract class AuthWebAuthnCredential
with AWSSerializable<Map<String, Object?>> {
/// {@macro amplify_core.auth_webauthn_credential}
const AuthWebAuthnCredential();

/// Unique identifier for this credential.
String get credentialId;

/// User-assigned friendly name for the credential (e.g., "My iPhone").
String? get friendlyName;

/// Relying party identifier (typically the domain).
String get relyingPartyId;

/// Type of authenticator attachment (e.g., "platform", "cross-platform").
String? get authenticatorAttachment;

/// List of transport types supported by the authenticator (e.g., "usb", "nfc", "ble", "internal").
List<String>? get authenticatorTransports;

/// Date and time when the credential was created.
DateTime get createdAt;

/// Converts the instance to a JSON map.
@override
Map<String, Object?> toJson();

@override
String toString() {
return 'AuthWebAuthnCredential{credentialId=$credentialId, friendlyName=$friendlyName, relyingPartyId=$relyingPartyId, createdAt=$createdAt}';
}
}
Loading
Loading