Using the AWS Amplify to integrate with Amazon Cognito is one of the fastest ways to add authentication and authorisation in any web or mobile app. We can either leverage Amplify Auth’s API capabilities to build our own auth flow or we can just use its pre-built UI components. At the time of writing this post, Amplify UI components support React, React Native, Vue, Angular and Web Components.
The goal of this post is to share how we can customise AmplifySignIn and AmplifySignUp UI components to add custom form fields and validations. I am using React as my chosen framework, but this could be done in other frameworks as well.
What are we building
We will add custom form fields and custom validations to AmplifySignIn and AmplifySignUp components but still reuse the code as much as possible to leverage the built in auth flow. Default Amplify Auth Sign up form looks like the below image:
We will add first name, last name and confirm password fields to the default form. The phone number field will be changed to only accept the UK phone number without the country code prefix. Then we will add validation to each field and the resulting sign up form looks like below:
Configuring the Amplify CLI is outside the scope of this post. You can follow the documentation here.
Create authentication service using AWS Amplify
Once the AWS Amplify CLI is configured, we can add authentication service to start building our authentication app. I have created a simple react app using the create react app command. The GitHub link of the full source code is available at the end of this post.
Once the react app is up and running, we initialise AWS Amplify from the root of the project:
amplify init
The above command configures the app to use the amplify backend. It creates the amplify directory inside the project root, creates the aws-export.js file to the src folder and adds few entries in the .gitignore file.
Next, we have to install amplify libraries:
npm install aws-amplify @aws-amplify/ui-react
Now, we will add/create authentication service for our app.
amplify add auth
The above configures the auth service for our app.
We will deploy our auth service using the push command:
amplify push
Render the default Sign in and Sign up form
At first, we will use AmplifySignIn and AmplifySignUp components to render the default view. I have modified the App.js file as below:
import "./App.css";
import Amplify from "aws-amplify";
import {
AmplifyAuthenticator, AmplifySignIn, AmplifySignUp
} from "@aws-amplify/ui-react";
import awsconfig from "./aws-exports";
Amplify.configure(awsconfig);
function App() {
return (
<div>
<AmplifyAuthenticator usernameAlias="email">
<AmplifySignIn/>
<AmplifySignUp/>
</AmplifyAuthenticator>
</div>
);
}
export default App;
The App component imports required amplify modules to render the AmplifySignIn and AmplifySignUp components. This is how the default sign in form looks like:
Customising the Sign in form
The customised Sign in form will have same email and password fields, but with different styles. We will highlight the fields in red when there is a validation error and validation messages will appear at the top:
We will first create our own login component, then, use the AmplifySignIn component inside our login component. So, let's add Login.js file inside the components folder. Amplify UI components use the web component’s slot feature. The parent AmplifyAuthenticator component has a slot named “sign-in”, which we can use to render the login component inside the AmplifyAuthenticator component. The return statement of our Login component now looks as below:
return (
<div ref={setAmplifySignInRef} slot="sign-in">
<AmplifySignIn formFields={formFields()}>
<div slot="header-subtitle">
{!email.valid && email.focused && (
<ValidationMessage message="Please enter a valid email
address" />
)}
{!password.valid && password.focused && (
<ValidationMessage message="Please enter a valid
password" />
)}
</div>
<AmplifyButton
slot="primary-footer-content"
type="button"
data-test="sign-in-sign-in-button"
handleButtonClick={handleSubmit}
>
Sign In
</AmplifyButton>
</AmplifySignIn>
</div>
);
As shown above, AmplifySignIn component accepts formFields props which takes an array of form field object. This allows us to customise the style and behaviour of each form field. Each form field takes an object called inputProps. InputProps are standard html input attributes. The handleValidation function passed as an input props checks for the validity of the field when the field loses its focus.
const formFields = () => {
return [
{
type: "email",
label: constants.EMAIL_LABEL,
placeholder: constants.EMAIL_PLACEHOLDER,
value: email.value,
inputProps: {
autocomplete: "off",
onBlur: (e) => {
handleValidation({
ev: e,
rules: { required: true },
});
},
style:
!email.valid && email.focused ? errorStyle : null,
},
},
{
type: "password",
label: constants.PASSWORD_LABEL,
placeholder: constants.PASSWORD_PLACEHOLDER,
value: password.value,
inputProps: {
autocomplete: "off",
style:
!password.valid && password.focused
? errorStyle
: null,
onblur: (e) =>
handleValidation({
rules: { required: true },
ev: e,
}),
},
},
];
};
Validation messages are rendered inside the header-subtitle slot of the AmplifySignIn component. The handleValidation function, as shown below, dispatches a reducer that sets the validation state of the form.
const handleValidation = ({ ev, rules }) => {
const { value, type, name } = ev.target;
dispatch({ type, name, rules, value });
};
We are using the AmplifyButton component which takes handleSubmit function as handleButtonClick props. The handleSubmit function checks the form validity before handing it over to AmplifySignIn component’s handleSubmit function.
We store the reference of the AmplifySignIn component using useRef hook. This might not be considered as best practice, however, in this case, it allows us to use the built in form submission logic of the AmplifySignIn component. Thus, we avoid writing complex logic to handle the auth flow.
Storing the reference of AmplifySignInComponent:
const amplifySignInRef = useRef();
const setAmplifySignInRef = (node) => {
if (node) {
const array = [...node.children];
if (array.some((val) => val.nodeName === "AMPLIFY-SIGN-IN"))
{
amplifySignInRef.current = array.find(
(val) => val.nodeName === "AMPLIFY-SIGN-IN"
);
}
}
};
Below shows how the reference of the AmplifySignInComponent is used to submit the form:
const handleSubmit = (ev) => {
ev.preventDefault();
if (!isFormValid) {
dispatch({ type: "submit" });
return;
}
amplifySignInRef.current.handleSubmit(ev);
};
Customising the Sign up form
Sign up form customisations are almost same as we have done in the sign in form. We will reuse the AmplifySignUp component inside our newly created Signup component. We add firstname, lastname, confirmPassword and phone fields to the formFields array to pass it to the formFields props of the AmplifySignUp component.
Validations work the same way as we have done in the SignIn component. Validation messages render inside the header-subtitle slot. The below code block shows the full return statement of the SignUp component:
return (
<div slot="sign-up" ref={setAmplifySignUpRef}>
<AmplifySignUp formFields={formFields()} handleSubmit={handleSubmit}>
<div slot="header-subtitle">
{!email.valid && email.focused && (
<ValidationMessage message="Please enter a valid email address" />
)}
{(!password.valid || !confirmPassword.valid) &&
(password.focused || confirmPassword.focused) && (
<ValidationMessage message="Please enter and confirm your password (minimum 8 characters with at least one number)" />
)}
{!firstname.valid && firstname.focused && (
<ValidationMessage message="Please enter your firstname" />
)}
{!lastname.valid && lastname.focused && (
<ValidationMessage message="Please enter your lastname" />
)}
{!phone.valid && phone.focused && (
<ValidationMessage message="Please enter a valid UK phone number" />
)}
</div>
</AmplifySignUp>
</div>
);
Due to adding extra fields we are not able to use AmplifySignUp component’s default submit handler, instead we are using Auth module from “@aws-amplify/auth” to call the SignUp api. We are storing the AmplifySignUp component reference using useRef hook. This reference is used to call the handleAuthStateChange function to hand the auth flow back to the AmplifySignUp component. Thus, we avoid creating custom logic for auth state handling.
const handleSubmit = async (ev) => {
ev.preventDefault();
if (!isFormValid) {
dispatch({ type: "submit" });
return;
}
try {
const authData = {
username: email.value,
password: password.value,
attributes: {
email: email.value,
phone_number: `+44${phone.value}`,
given_name: firstname.value,
family_name: lastname.value,
},
};
const data = await Auth.signUp(authData);
if (data.userConfirmed) {
await handleSignIn(
email.value,
password.value,
amplifySignUpRef.current.handleAuthStateChange
);
} else {
const signUpAttrs = { ...authData };
amplifySignUpRef.current.handleAuthStateChange(
AuthState.ConfirmSignUp,
{
...data.user,
signUpAttrs,
}
);
}
} catch (error) {
dispatch({ type: "error" });
dispatchToastHubEvent(error);
}
};
Finally, the app component looks as below:
import "./App.css";
import Amplify from "aws-amplify";
import {
AmplifyAuthenticator,
} from "@aws-amplify/ui-react";
import Signup from "./components/Signup";
import useMuiStyles from "./hooks/useMuiStyle";
import Login from "./components/Login";
import ErrorDialogue from "./components/common/ErrorDialogue";
import awsconfig from "./aws-exports";
Amplify.configure(awsconfig);
function App() {
const classes = useMuiStyles();
return (
<div className={classes.root}>
<ErrorDialogue/>
<AmplifyAuthenticator usernameAlias="email" hideToast={true}>
<Login/>
<Signup/>
</AmplifyAuthenticator>
</div>
);
}
export default App;
Conclusion
Though storing the component reference might not be considered as optimum solution in most cases, in this case, it helps us with the benefit of maximum reuse and quick customisation. If the requirement is to build the minimum viable product with auth functionalities, AWS Amplify UI components are serious contenders for consideration. This post shows these components could be customised quickly and easily to get the maximum benefit out of it.
You can download the source code from here
Top comments (1)