Skip to content

Commit

Permalink
Cognito auth 6/7 - add signout (#5867)
Browse files Browse the repository at this point in the history
  • Loading branch information
indiv0 authored Mar 21, 2023
1 parent 210b5b7 commit 0fc8683
Show file tree
Hide file tree
Showing 15 changed files with 584 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,15 @@ const GITHUB_PROVIDER = 'Github'

const MESSAGES = {
signInWithPassword: {
userNotFound: 'User not found. Please register first.',
userNotFound: 'Username not found. Please register first.',
userNotConfirmed: 'User is not confirmed. Please check your email for a confirmation link.',
incorrectUsernameOrPassword: 'Incorrect username or password.',
},
forgotPassword: {
userNotFound: 'Username not found. Please register first.',
userNotConfirmed:
'Cannot reset password for user with an unverified email. Please verify your email first.',
},
}

// ====================
Expand Down Expand Up @@ -98,6 +103,25 @@ function intoAmplifyErrorOrThrow(error: unknown): AmplifyError {
}
}

// =================
// === AuthError ===
// =================

/** Object returned by the AWS Amplify library when an auth error occurs. */
interface AuthError {
name: string
log: string
}

/** Hints to TypeScript if we can safely cast an `unknown` error to an `AuthError`. */
function isAuthError(error: unknown): error is AuthError {
if (error && typeof error === 'object') {
return 'name' in error && 'log' in error
} else {
return false
}
}

// ===============
// === Cognito ===
// ===============
Expand All @@ -107,7 +131,6 @@ function intoAmplifyErrorOrThrow(error: unknown): AmplifyError {
* The caller can then handle them via pattern matching on the {@link results.Result} type. */
export class Cognito {
constructor(
// @ts-expect-error This will be used in a future PR.
private readonly logger: loggerProvider.Logger,
private readonly platform: platformModule.Platform,
amplifyConfig: config.AmplifyConfig
Expand Down Expand Up @@ -169,6 +192,29 @@ export class Cognito {
return signInWithPassword(username, password)
}

/** Signs out the current user. */
signOut() {
return signOut(this.logger)
}

/** Sends a password reset email.
*
* The user will be able to reset their password by following the link in the email, which takes
* them to the "reset password" page of the application. The verification code will be filled in
* automatically. */
forgotPassword(email: string) {
return forgotPassword(email)
}

/** Submits a new password for the given email address.
*
* The user will have received a verification code in an email, which they will have entered on
* the "reset password" page of the application. This function will submit the new password
* along with the verification code, changing the user's password. */
forgotPasswordSubmit(email: string, code: string, password: string) {
return forgotPasswordSubmit(email, code, password)
}

/** We want to signal to Amplify to fire a "custom state change" event when the user is
* redirected back to the application after signing in via an external identity provider. This
* is done so we get a chance to fix the location history. The location history is the history
Expand Down Expand Up @@ -294,7 +340,7 @@ const SIGN_UP_USERNAME_EXISTS_ERROR = {
} as const

const SIGN_UP_INVALID_PARAMETER_ERROR = {
internalCode: 'InvalidParameterEx[ception',
internalCode: 'InvalidParameterException',
kind: 'InvalidParameter',
} as const

Expand Down Expand Up @@ -428,3 +474,103 @@ function intoSignInWithPasswordErrorOrThrow(error: AmplifyError): SignInWithPass
throw error
}
}

// ======================
// === ForgotPassword ===
// ======================
const FORGOT_PASSWORD_USER_NOT_CONFIRMED_ERROR = {
internalCode: 'InvalidParameterException',
kind: 'UserNotConfirmed',
message:
'Cannot reset password for the user as there is no registered/verified email or phone_number',
} as const

async function forgotPassword(email: string) {
return results.Result.wrapAsync(async () => {
await amplify.Auth.forgotPassword(email)
})
.then(result => result.mapErr(intoAmplifyErrorOrThrow))
.then(result => result.mapErr(intoForgotPasswordErrorOrThrow))
}

type ForgotPasswordErrorKind = 'UserNotConfirmed' | 'UserNotFound'

export interface ForgotPasswordError {
kind: ForgotPasswordErrorKind
message: string
}

function intoForgotPasswordErrorOrThrow(error: AmplifyError): ForgotPasswordError {
if (error.code === 'UserNotFoundException') {
return {
kind: 'UserNotFound',
message: MESSAGES.forgotPassword.userNotFound,
}
} else if (
error.code === FORGOT_PASSWORD_USER_NOT_CONFIRMED_ERROR.internalCode &&
error.message === FORGOT_PASSWORD_USER_NOT_CONFIRMED_ERROR.message
) {
return {
kind: FORGOT_PASSWORD_USER_NOT_CONFIRMED_ERROR.kind,
message: MESSAGES.forgotPassword.userNotConfirmed,
}
} else {
throw error
}
}

// ============================
// === ForgotPasswordSubmit ===
// ============================

async function forgotPasswordSubmit(email: string, code: string, password: string) {
return results.Result.wrapAsync(async () => {
await amplify.Auth.forgotPasswordSubmit(email, code, password)
}).then(result => result.mapErr(intoForgotPasswordSubmitErrorOrThrow))
}

type ForgotPasswordSubmitErrorKind = 'AmplifyError' | 'AuthError'

export interface ForgotPasswordSubmitError {
kind: ForgotPasswordSubmitErrorKind
message: string
}

function intoForgotPasswordSubmitErrorOrThrow(error: unknown): ForgotPasswordSubmitError {
if (isAuthError(error)) {
return {
kind: 'AuthError',
message: error.log,
}
} else if (isAmplifyError(error)) {
return {
kind: 'AmplifyError',
message: error.message,
}
} else {
throw error
}
}

// ===============
// === SignOut ===
// ===============

async function signOut(logger: loggerProvider.Logger) {
// FIXME [NP]: https://github.com/enso-org/cloud-v2/issues/341
// For some reason, the redirect back to the IDE from the browser doesn't work correctly so this
// `await` throws a timeout error. As a workaround, we catch this error and force a refresh of
// the session manually by running the `signOut` again. This works because Amplify will see that
// we've already signed out and clear the cache accordingly. Ideally we should figure out how
// to fix the redirect and remove this `catch`. This has the unintended consequence of catching
// any other errors that might occur during sign out, that we really shouldn't be catching. This
// also has the unintended consequence of delaying the sign out process by a few seconds (until
// the timeout occurs).
try {
await amplify.Auth.signOut()
} catch (error) {
logger.error('Sign out failed', error)
} finally {
await amplify.Auth.signOut()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ import * as fontawesomeIcons from "@fortawesome/free-brands-svg-icons";

import * as icons from "../../components/svg";

// =============
// === Input ===
// =============

export function Input(props: React.InputHTMLAttributes<HTMLInputElement>) {
return (
<input
{...props}
className={
"text-sm sm:text-base placeholder-gray-500 pl-10 pr-4 rounded-lg border border-gray-400 " +
"w-full py-2 focus:outline-none focus:border-blue-400"
}
/>
);
}

// ===============
// === SvgIcon ===
// ===============
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/** @file Container responsible for rendering and interactions in first half of forgot password
* flow. */
import * as router from "react-router-dom";

import * as app from "../../components/app";
import * as auth from "../providers/auth";
import * as common from "./common";
import * as hooks from "../../hooks";
import * as icons from "../../components/svg";
import * as utils from "../../utils";

// ======================
// === ForgotPassword ===
// ======================

function ForgotPassword() {
const { forgotPassword } = auth.useAuth();

const [email, bindEmail] = hooks.useInput("");

return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
<div
className={
"flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full " +
"max-w-md"
}
>
<div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800">
Forgot Your Password?
</div>
<div className="mt-10">
<form
onSubmit={utils.handleEvent(async () => {
await forgotPassword(email);
})}
>
<div className="flex flex-col mb-6">
<label
htmlFor="email"
className="mb-1 text-xs sm:text-sm tracking-wide text-gray-600"
>
E-Mail Address:
</label>
<div className="relative">
<common.SvgIcon data={icons.PATHS.at} />

<common.Input
{...bindEmail}
id="email"
type="email"
name="email"
placeholder="E-Mail Address"
/>
</div>
</div>
<div className="flex w-full">
<button
type="submit"
className={
"flex items-center justify-center focus:outline-none text-white text-sm " +
"sm:text-base bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition " +
"duration-150 ease-in"
}
>
<span className="mr-2 uppercase">Send link</span>
<span>
<icons.Svg data={icons.PATHS.rightArrow} />
</span>
</button>
</div>
</form>
</div>
<div className="flex justify-center items-center mt-6">
<router.Link
to={app.LOGIN_PATH}
className={
"inline-flex items-center font-bold text-blue-500 hover:text-blue-700 text-xs " +
"text-center"
}
>
<span>
<icons.Svg data={icons.PATHS.goBack} />
</span>
<span className="ml-2">Go back to login</span>
</router.Link>
</div>
</div>
</div>
);
}

export default ForgotPassword;
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ const BUTTON_CLASS_NAME =
"relative mt-6 border rounded-md py-2 text-sm text-gray-800 " +
"bg-gray-100 hover:bg-gray-200";

const INPUT_CLASS_NAME =
"text-sm sm:text-base placeholder-gray-500 pl-10 pr-4 rounded-lg border " +
"border-gray-400 w-full py-2 focus:outline-none focus:border-blue-400";

const LOGIN_QUERY_PARAMS = {
email: "email",
} as const;
Expand Down Expand Up @@ -87,13 +83,12 @@ function Login() {
<div className="relative">
<common.SvgIcon data={icons.PATHS.at} />

<input
<common.Input
{...bindEmail}
required={true}
id="email"
type="email"
name="email"
className={INPUT_CLASS_NAME}
placeholder="E-Mail Address"
/>
</div>
Expand All @@ -108,18 +103,28 @@ function Login() {
<div className="relative">
<common.SvgIcon data={icons.PATHS.lock} />

<input
<common.Input
{...bindPassword}
required={true}
id="password"
type="password"
name="password"
className={INPUT_CLASS_NAME}
placeholder="Password"
/>
</div>
</div>

<div className="flex items-center mb-6 -mt-4">
<div className="flex ml-auto">
<router.Link
to={app.FORGOT_PASSWORD_PATH}
className="inline-flex text-xs sm:text-sm text-blue-500 hover:text-blue-700"
>
Forgot Your Password?
</router.Link>
</div>
</div>

<div className="flex w-full">
<button
type="submit"
Expand Down
Loading

0 comments on commit 0fc8683

Please sign in to comment.