Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(auth): add js passwordless changes #8129

Merged
merged 16 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,10 @@ The `signIn` API response will include a `nextStep` property, which can be used
| `CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED` | The user was created with a temporary password and must set a new one. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE` | The sign-in must be confirmed with a custom challenge response. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_TOTP_CODE` | The sign-in must be confirmed with a TOTP code from the user. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_SMS_CODE` | The sign-in must be confirmed with a SMS code from the user. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_EMAIL_CODE` | The sign-in must be confirmed with a EMAIL code from the user. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_SMS_CODE` | The sign-in must be confirmed with an SMS code from the user. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_EMAIL_CODE` | The sign-in must be confirmed with an EMAIL code from the user. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_PASSWORD` | The sign-in must be confirmed with the password from the user. Complete the process with `confirmSignIn`. |
| `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION` | The user must select their mode of first factor authentication. Complete the process by passing the desired mode to the `challengeResponse` field of `confirmSignIn`. |
| `CONTINUE_SIGN_IN_WITH_MFA_SELECTION` | The user must select their mode of MFA verification before signing in. Complete the process with `confirmSignIn`. |
| `CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION` | The user must select their mode of MFA verification to setup. Complete the process by passing either `"EMAIL"` or `"TOTP"` to `confirmSignIn`. |
| `CONTINUE_SIGN_IN_WITH_TOTP_SETUP` | The TOTP setup process must be continued. Complete the process with `confirmSignIn`. |
Expand Down Expand Up @@ -589,6 +591,7 @@ Following sign in, you will receive a `nextStep` in the sign-in result of one of
| `CONFIRM_SIGN_IN_WITH_TOTP_CODE` | The sign-in must be confirmed with a TOTP code from the user. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_SMS_CODE` | The sign-in must be confirmed with a SMS code from the user. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_EMAIL_CODE` | The sign-in must be confirmed with a EMAIL code from the user. Complete the process with `confirmSignIn`. |
| `CONFIRM_SIGN_IN_WITH_PASSWORD` | The sign-in must be confirmed with the password from the user. Complete the process with `confirmSignIn`. |
jjarvisp marked this conversation as resolved.
Show resolved Hide resolved
| `CONTINUE_SIGN_IN_WITH_MFA_SELECTION` | The user must select their mode of MFA verification before signing in. Complete the process with `confirmSignIn`. |
| `CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION` | The user must select their mode of MFA verification to setup. Complete the process by passing either `"EMAIL"` or `"TOTP"` to `confirmSignIn`. |
| `CONTINUE_SIGN_IN_WITH_TOTP_SETUP` | The TOTP setup process must be continued. Complete the process with `confirmSignIn`. |
Expand Down Expand Up @@ -1103,13 +1106,148 @@ func socialSignInWithWebUI() -> AnyCancellable {

Your application's users can also sign in using passwordless methods. To learn more, visit the [concepts page for passwordless](/[platform]/build-a-backend/auth/concepts/passwordless/)

<InlineFilter filters={["angular", "javascript", "nextjs", "react", "react-native", "vue"]}>

### New auth flow type

In order to facilitate the new passwordless sign in options, Cognito is introducing a new auth flow type known as `USER_AUTH`. This flow is designed to be flexible and supports both password and passwordless sign in factors.

```ts
const { nextStep } = await signIn({
username: state.username,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is off here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this info should be on the switching authentication flows page I believe.

options: {
authFlowType: "USER_AUTH",
}
});
```

### Sign In with a Preferred Challenge for First Factor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, be careful with headers, we created 3 new sections on this page. Should only be SMS OTP, Email OTP, WebAuthn Passkeys, and Password or SRP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we restricted to those 3? I think giving details on the new USER_AUTH flow is a necessary prerequisite to handle the new passwordless methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to be consistent with the other libraries. We'd provide conceptual introduction to the auth flow on the Switching Authentication Flows page.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I'll move it there - that section seems like a logical place for it to live in our doc structure (though I would still prefer it together 🙃)

Copy link
Member

@jjarvisp jjarvisp Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good compromise might be to add a <Callout /> mentioning USER_AUTH with a link to that page


The USER_AUTH sign in flow will support the following methods of first factor authentication: `WEB_AUTHN`, `EMAIL_OTP`, `SMS_OTP`, `PASSWORD`, and `PASSWORD_SRP`.
scanlonp marked this conversation as resolved.
Show resolved Hide resolved

If the desired first factor is known before the sign in flow is initiated it can be passed to the initial sign in call:

```ts
type AuthFactorType =
| 'WEB_AUTHN'
| 'EMAIL_OTP'
| 'SMS_OTP'
| 'PASSWORD'
| 'PASSWORD_SRP';

type SignInOptions = AuthServiceOptions & {
authFlowType?: AuthFlowType;
clientMetadata?: ClientMetadata;
preferredChallenge?: AuthFactorType;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be confusing to have types here that aren't referenced in the examples; the connection between these types and how to pass as options is unclear. Is there a way we can clarify this? Maybe using the SignInInput interface which is publicly exposed by the library... or just relying on the examples?

```

```ts
// PASSWORD_SRP / PASSWORD
// sign in with preferred challenge as password
// note password must be provided in same step
const { nextStep } = await signIn({
username: "[email protected]",
password: "hunter2",
options: {
authFlowType: "USER_AUTH",
preferredChallenge: "PASSWORD_SRP" // or "PASSWORD"
},
});

// nextStep.signInStep === 'DONE'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nextStep may not be DONE in all cases. We should provide a partial implementation of the handleNextSignInStep() handler

handleNextSignInStep(nextStep);

// WEB_AUTHN
// sign in with preferred challenge as web authn
// no user input required at this step
const { nextStep } = await signIn({
username: "[email protected]",
options: {
authFlowType: "USER_AUTH",
preferredChallenge: "WEB_AUTHN"
},
});

// nextStep.signInStep === 'DONE'
handleNextSignInStep(nextStep);

// EMAIL_OTP
// sign in with preferred challenge as email otp
// no user input required at this step
const { nextStep } = await signIn({
username: "[email protected]",
options: {
authFlowType: "USER_AUTH",
preferredChallenge: "EMAIL_OTP"
},
});

// nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE'
handleNextSignInStep(nextStep);

// SMS_OTP
// sign in with preferred challenge as sms otp
// no user input required at this step
const { nextStep } = await signIn({
username: "[email protected]",
options: {
authFlowType: "USER_AUTH",
preferredChallenge: "SMS_OTP"
},
});

// nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_SMS_CODE'
handleNextSignInStep(nextStep);
```

Note that if the preferred auth factor is not available, the preference will be ignored and the flow will continue to first factor discovery.
jjarvisp marked this conversation as resolved.
Show resolved Hide resolved

### Passwordless Sign In with First Factor Discovery

When multiple first factor options are available and none are set as preferred in InitiateAuth API call (or preferred challenge is not available), Amplify JS continues to a new sign in step `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION`:

```ts
interface ContinueSignInWithFirstFactorSelection {
signInStep: 'CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION';
availableChallenges?: ChallengeName[];
}
```

```ts
const { nextStep } = await signIn({
username: "[email protected]",
options: {
authFlowType: "USER_AUTH",
}
});

// nextStep.signInStep === 'CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION'
handleNextSignInStep(nextStep);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid including internal types in the documentation; we should show an example of how to use the availableChallenges instead.

```

To initiate an available first factor, the desired first factor type is passed to confirm sign in as the challenge response. The next sections will that step for each `AuthFactorType`.
scanlonp marked this conversation as resolved.
Show resolved Hide resolved

</InlineFilter>

### SMS OTP

{/* blurb with supplemental information about handling sign-in, events, etc. */}

<InlineFilter filters={["angular", "javascript", "nextjs", "react", "react-native", "vue"]}>

{/* */}
To request an OTP code via SMS for authentication, the challenge is passed as the challenge response to the confirm sign in API.

Amplify will respond appropriately to Cognito and return the challenge as sign in next step: `CONFIRM_SIGN_IN_WITH_SMS_CODE`:

```ts
const { nextStep } = await confirmSignIn({
challengeResponse: "SMS_OTP"
});

// nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_SMS_CODE'
handleNextSignInStep(nextStep);
```

</InlineFilter>
<InlineFilter filters={["android"]}>
Expand All @@ -1134,7 +1272,18 @@ Your application's users can also sign in using passwordless methods. To learn m

<InlineFilter filters={["angular", "javascript", "nextjs", "react", "react-native", "vue"]}>

{/* */}
To request an OTP code via email for authentication, the challenge is passed as the challenge response to the confirm sign in API.

Amplify JS will respond appropriately to Cognito and return the challenge as sign in next step: `CONFIRM_SIGN_IN_WITH_EMAIL_CODE`:
scanlonp marked this conversation as resolved.
Show resolved Hide resolved

```ts
const { nextStep } = await confirmSignIn({
challengeResponse: "EMAIL_OTP"
});

// nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE'
handleNextSignInStep(nextStep);
```

</InlineFilter>
<InlineFilter filters={["android"]}>
Expand All @@ -1159,7 +1308,18 @@ Your application's users can also sign in using passwordless methods. To learn m

<InlineFilter filters={["angular", "javascript", "nextjs", "react", "react-native", "vue"]}>

{/* */}
The WebAuthn credential flow is initiated by passing the challenge name to the confirm sign in api. Cognito will respond with credential request options that are used by Amplify JS to begin the authentication ceremony with the local authenticator.
scanlonp marked this conversation as resolved.
Show resolved Hide resolved

As no additional user input is required from the application to complete this flow, there is no intermediate sign in step specific to WebAuthn:
scanlonp marked this conversation as resolved.
Show resolved Hide resolved

```ts
const { nextStep } = await confirmSignIn({
challengeResponse: "WEB_AUTHN",
});

// nextStep.signInStep === 'DONE'
handleNextSignInStep(nextStep);
```

</InlineFilter>
<InlineFilter filters={["android"]}>
Expand All @@ -1177,3 +1337,28 @@ Your application's users can also sign in using passwordless methods. To learn m
{/* */}

</InlineFilter>

<InlineFilter filters={["angular", "javascript", "nextjs", "react", "react-native", "vue"]}>

### Password or SRP

Traditional password based authentication is available from this flow as well. To initiate this flow from select challenge, either `PASSWORD` or `PASSWORD_SRP` is passed as the challenge response.

```ts
const { nextStep } = await confirmSignIn({
challengeResponse: "PASSWORD_SRP", // or "PASSWORD"
});

// in both cases
// nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_PASSWORD'
handleNextSignInStep(nextStep);

const { nextStep: nextNextStep } = await confirmSignIn({
challengeResponse: "Test123#",
});

// nextNextStep.signInStep === 'DONE'
handleNextSignInStep(nextNextStep);
```

</InlineFilter>
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,131 @@ export function getStaticProps() {
};
}

<InlineFilter filters={["react-native"]}>
<Callout warning>

WebAuthn registration and authentication are not currently supported on React Native, other passwordless features are fully supported.

</Callout>
</InlineFilter>

Amplify Auth enables your users to associate, keep track of, and delete passkeys.

## Associate a passkey

Note that users must be authenticated to register a passkey. That also means users cannot create a passkey during sign up; consequently, they must have at least one other first factor authentication mechanism associated with their account to use WebAuthn.

You can associate a passkey using the following API:

<InlineFilter filters={["angular", "javascript", "nextjs", "react", "react-native", "vue"]}>

```ts
import { associateWebAuthnCredential} from 'aws-amplify/auth';

await associateWebAuthnCredential();
```

The associate passkey API has no inputs, and it does not return a value.
scanlonp marked this conversation as resolved.
Show resolved Hide resolved

</InlineFilter>

You will be prompted to register a passkey using your local authenticator. Amplify will then associate that passkey with Cognito.

## List passkeys

You can list registered passkeys using the following API:

<InlineFilter filters={["angular", "javascript", "nextjs", "react", "react-native", "vue"]}>

```ts
import { listWebAuthnCredentials } from 'aws-amplify/auth';

await listWebAuthnCredentials();
```

The list passkeys API takes optional parameters `pageSize` and `nextToken` which may be used to paginate the list request.

The API returns a list of `AuthWebAuthnCredential` objects, which define the properties of a passkey, as well as a `nextToken` to be used in subsequent list requests.

```ts
interface ListWebAuthnCredentialInput {
pageSize?: number;
nextToken?: string;
}

interface ListWebAuthnCredentialOutput {
credentials: AuthWebAuthnCredential[];
nextToken?: string;
}

interface AuthWebAuthnCredential {
credentialId: string | undefined;
friendlyCredentialName: string | undefined;
relyingPartyId: string | undefined;
authenticatorAttachment?: string;
authenticatorTransports: string[] | undefined;
createdAt: Date | undefined;
}
```

</InlineFilter>

## Delete a passkey

You can delete a passkey with the following API:

<InlineFilter filters={["angular", "javascript", "nextjs", "react", "react-native", "vue"]}>

```ts
import { deleteWebAuthnCredential } from 'aws-amplify/auth';

const id = "credential-id-to-delete";

await deleteWebAuthnCredential({
credentialId: id
});
```

The delete passkey API has only the required `credentialId` as input, and it does not return a value.

```ts
interface DeleteWebAuthnCredentialInput {
credentialId: string;
}
```

</InlineFilter>

<InlineFilter filters={["angular", "javascript", "nextjs", "react", "react-native", "vue"]}>

## Practical example

Here is a code example that uses the two APIs together. In this example, the user has 3 passkeys registered. They want to list all passkeys as well as delete the first passkey in the list.

```ts
import {
listWebAuthnCredentials,
deleteWebAuthnCredential
} from 'aws-amplify/auth';

let passkeys = [];

const result = await listWebAuthnCredentials({ pageSize: 2 });

passkeys.push(...result.credentials);

const nextPage = await listWebAuthnCredentials({
pageSize: 2,
nextToken: result.nextToken,
});

passkeys.push(...nextPage.credentials);

const id = passkeys[0].credentialId;

await deleteWebAuthnCredential({
credentialId: id
});
```

</InlineFilter>