Skip to content

chore(auth): add js passwordless changes #8129

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

Merged
merged 16 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -1103,13 +1103,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`.

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: state.username,
password: state.password,
Copy link
Contributor

Choose a reason for hiding this comment

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

can we use placeholder strings for these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean a variable with a placeholder string or string literals with a consistent placeholder string?

Copy link
Contributor

Choose a reason for hiding this comment

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

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: state.username,
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: state.username,
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: state.username,
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.

### 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: state.username,
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`.

</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 JS 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 +1269,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`:

```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 +1305,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.

As no additional user input is required from the application to complete this flow, there is no intermediate sign in step specific to WebAuthn:

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

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

</InlineFilter>
<InlineFilter filters={["android"]}>
Expand All @@ -1177,3 +1334,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,111 @@ export function getStaticProps() {
};
}

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

Passwordless features are currently not supported on react native.

</Callout>
</InlineFilter>

Amplify Auth enables your users to keep track of registered passkeys.

## 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>