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

feat(auth): passwordless sign in and sign up for OTP #12711

Draft
wants to merge 4 commits into
base: feat/passwordless-auth
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,3 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { Amplify, Hub } from '@aws-amplify/core';
import { respondToAuthChallenge } from '../utils/clients/CognitoIdentityProvider';
import {
Expand Down Expand Up @@ -59,8 +62,8 @@ export const confirmSignInWithOTP = async (
},
Session: signInSession,
ClientMetadata: {
signInMethod: 'OTP',
action: 'CONFIRM',
"Amplify.Passwordless.signInMethod": "OTP",
"Amplify.Passwordless.action": "CONFIRM",
},
ClientId: userPoolClientId,
};
Expand Down
@@ -0,0 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export {handlePasswordlessSignIn} from "./passwordlessSignIn";
export {createUserForPasswordlessSignUp} from "./passwordlessCreateUser";
export {getDeliveryMedium, parseApiServiceError} from "./utils";
export {PasswordlessSignInPayload, PreInitiateAuthPayload, PasswordlessSignUpPayload} from './types';
@@ -0,0 +1,81 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {
HttpRequest,
unauthenticatedHandler,
Headers,
getRetryDecider,
jitteredBackoff,
} from '@aws-amplify/core/internals/aws-client-utils';
import { normalizeHeaders } from "../../utils/apiHelpers";
import { getDeliveryMedium, parseApiServiceError } from "./utils";
import { getRegion } from "../../utils/clients/CognitoIdentityProvider/utils";
import { AuthUserAttributes } from "../../../../types";
import { PreInitiateAuthPayload, PasswordlessSignUpPayload } from './types';

/**
* Internal method to create a user when signing up passwordless.
*/
export async function createUserForPasswordlessSignUp(
payload: PasswordlessSignUpPayload,
userPoolId: string,
userAttributes?: AuthUserAttributes
){

const { email, phone_number, username, destination} = payload

// pre-auth api request
const body: PreInitiateAuthPayload = {
phone_number: phone_number,
email: email,
username: username,
deliveryMedium: getDeliveryMedium(destination),
region: getRegion(userPoolId),
userPoolId: userPoolId,
userAttributes,
};

const resolvedBody = body
? body instanceof FormData
? body
: JSON.stringify(body ?? '')
: undefined;

const headers: Headers = {};

const resolvedHeaders: Headers = {
...normalizeHeaders(headers),
...(resolvedBody
? {
'content-type':
body instanceof FormData
? 'multipart/form-data'
: 'application/json; charset=UTF-8',
}
: {}),
};

const method = 'PUT';

// TODO: url should come from the config
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is where the update needs to happen after the work on the backend completes.

const url = new URL(
'https://8bzzjguuck.execute-api.us-west-2.amazonaws.com/prod'
);
const request: HttpRequest = {
url,
headers: resolvedHeaders,
method,
body: resolvedBody,
};
const baseOptions = {
retryDecider: getRetryDecider(parseApiServiceError),
computeDelay: jitteredBackoff,
withCrossDomainCredentials: false,
};

// creating a new user on Cognito via API endpoint
return await unauthenticatedHandler(request, {
...baseOptions,
});
}
@@ -0,0 +1,92 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { getAuthUserAgentValue } from "../../../../utils";
import { CognitoAuthSignInDetails } from "../../types/models";
import { initiateAuth, respondToAuthChallenge } from "../../utils/clients/CognitoIdentityProvider";
import { InitiateAuthCommandInput, RespondToAuthChallengeCommandInput } from "../../utils/clients/CognitoIdentityProvider/types";
import { getRegion } from "../../utils/clients/CognitoIdentityProvider/utils";
import { getActiveSignInUsername, setActiveSignInUsername } from "../../utils/signInHelpers";
import { AuthConfig } from '@aws-amplify/core';
import {
AuthAction
} from '@aws-amplify/core/internals/utils';
import { getDeliveryMedium } from "./utils";
import { setActiveSignInState } from "../../utils/signInStore";
import { PasswordlessSignInPayload } from "./types";

/**
* Internal method to perform passwordless sign in via both otp and magic link.
*/
export async function handlePasswordlessSignIn(
payload: PasswordlessSignInPayload,
authConfig: AuthConfig['Cognito']
) {
const { userPoolId, userPoolClientId } = authConfig;
const { username, clientMetadata, destination, signInMethod } = payload;
const authParameters: Record<string, string> = {
USERNAME: username,
};

const jsonReqInitiateAuth: InitiateAuthCommandInput = {
AuthFlow: 'CUSTOM_AUTH',
AuthParameters: authParameters,
ClientId: userPoolClientId,
};

// Intiate Auth with a custom flow
const { Session, ChallengeParameters } = await initiateAuth(
{
region: getRegion(userPoolId),
userAgentValue: getAuthUserAgentValue(AuthAction.SignIn),
},
jsonReqInitiateAuth
);
const activeUsername = ChallengeParameters?.USERNAME ?? username;

setActiveSignInUsername(activeUsername);

// The answer is not used by the service. It is just a placeholder to make the request happy.
const dummyAnswer = 'dummyAnswer';
const jsonReqRespondToAuthChallenge: RespondToAuthChallengeCommandInput = {
ChallengeName: 'CUSTOM_CHALLENGE',
ChallengeResponses: {
USERNAME: activeUsername,
ANSWER: dummyAnswer,
},
Session,
ClientMetadata: {
...clientMetadata,
'Amplify.Passwordless.signInMethod': signInMethod,
'Amplify.Passwordless.action': 'REQUEST',
'Amplify.Passwordless.deliveryMedium': getDeliveryMedium(destination),
},
ClientId: userPoolClientId,
};

// Request the backend to send code/link to the destination address
const responseFromAuthChallenge = await respondToAuthChallenge(
{
region: getRegion(userPoolId),
userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn),
},
jsonReqRespondToAuthChallenge
);

const signInDetails: CognitoAuthSignInDetails = {
loginId: username,
authFlowType: 'CUSTOM_WITHOUT_SRP',
};

// sets up local state used during the sign-in process
setActiveSignInState({
signInSession: responseFromAuthChallenge.Session,
username: getActiveSignInUsername(username),
signInDetails,
});

return responseFromAuthChallenge;
}



58 changes: 58 additions & 0 deletions packages/auth/src/providers/cognito/apis/passwordless/types.ts
@@ -0,0 +1,58 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { AuthPasswordlessDeliveryDestination, ClientMetadata } from "../../types/models";

/**
* Payload sent to the backend when creating a user.
*/
export type PasswordlessSignUpPayload = {
destination: AuthPasswordlessDeliveryDestination;
email?: string;
phone_number?: string;
username: string;
}

/**
* Payload sent to the backend when signing a user in.
*/
export type PasswordlessSignInPayload = ({
signInMethod: 'MAGIC_LINK';
destination: Extract<AuthPasswordlessDeliveryDestination, "EMAIL">;
} | {
signInMethod: 'OTP';
destination: AuthPasswordlessDeliveryDestination;
}) & {
username: string;
clientMetadata?: ClientMetadata;
}

export type PreInitiateAuthPayload = {
/**
* Optional fields to indicate where the code/link should be delivered.
*/
email?: string;
phone_number?: string;
username: string;

/**
* Any optional user attributes that were provided during sign up.
*/
userAttributes?: { [name: string]: string | undefined };

/**
* The delivery medium for passwordless sign in. For magic link this will
* always be "EMAIL". For OTP, it will be the value provided by the customer.
*/
deliveryMedium: string;

/**
* The user pool ID
*/
userPoolId: string;

/**
* The user pool region
*/
region: string;
};
28 changes: 28 additions & 0 deletions packages/auth/src/providers/cognito/apis/passwordless/utils.ts
@@ -0,0 +1,28 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { HttpResponse, parseJsonError } from "@aws-amplify/core/internals/aws-client-utils";
import { AuthPasswordlessDeliveryDestination } from "../../types/models";
import { MetadataBearer } from "@aws-amplify/core/dist/esm/clients/types/aws";

export function getDeliveryMedium(destination: AuthPasswordlessDeliveryDestination) {
const deliveryMediumMap: Record<AuthPasswordlessDeliveryDestination, string> =
{
EMAIL: 'EMAIL',
PHONE: 'SMS',
};
return deliveryMediumMap[destination];
}

export const parseApiServiceError = async (
response?: HttpResponse
): Promise<(Error & MetadataBearer) | undefined> => {
const parsedError = await parseJsonError(response);
if (!parsedError) {
// Response is not an error.
return;
}
return Object.assign(parsedError, {
$metadata: parsedError.$metadata,
});
};