Now it’s a lot easier to get some of the great AWS services on the client (securing storage with ease, Secure Lambda API calls with API Gateway, etc). Amplify has two main benefits:
Amplify-CLI
will generate all the security and permissions automatically avoiding a lot of manual setupYou can use the Amplify Vue components on top of the core library which should handle most use cases. If you do decide to build your own components, it is probably for one of the following reasons:
This article will:
For full customization, we will need to add our own user pool, but first use the client to set up all the permissions and services.
@aws-amplify/cli
)amplify init
amplify add auth
Storage
amplify push
User Pool
with the desired settingsemail verification messages
since this solution requires email confirmationApp clients
link and click Add another app client
Generate client secret
since this only applies to server-to-server communication:Amplify will generate an [aws-exports.js](https://github.com/nowpapercom/gotome-admin/blob/master/src/aws-exports.js "aws-exports.js")
file but we can’t rely on it not getting overwritten, also you should add it to your .gitignore
for security. In your main.js
file add the amplify dependencies:
import Amplify from 'aws-amplify'
const awsExports = {
// found in your generated aws-export.js file:
aws_project_region: 'us-west-2',
aws_cognito_identity_pool_id: 'us-west-2:00000000-0000-0000-0000-000000000000',
// your new user pool region
aws_cognito_region: 'us-west-2',
// 'Pool Id' found under user pool 'General settings'
aws_user_pools_id: 'us-west-2_000000000',
// New 'App client id' under user pool 'App clients'
aws_user_pools_web_client_id: '00000000000000000000000000',
// Any other Amplify services settings
}
Amplify.configure(awsExports)
vue-customizing-amplify-authentication-settings.js
For the purpose of this demo I am placing secrets in code, never add this to your repo, use environment variables and secret storage.
This Vuex module will do all the heavy lifting so your components deal with an easy API and you can easily check on the users’ authenticated state.
ogressiveMedia-canvas">
// Dependencies ===============
import {Auth} from 'aws-amplify'
const store = {namespaced: true}
// State ======================
store.state = {
authorized: false,
user: null,
loginError: '',
signupError: '',
confirm: false,
confirmError: '',
}
// Mutations ==================
store.mutations = {
user(state, user){
state.authorized = !!user && user.attributes && user.attributes.email_verified
state.user = user
},
confirm(state, showConfirm){
state.confirm = !!showConfirm
},
}
// Actions ====================
store.actions = {
async login({dispatch, state}, {email, password}){
state.loginError = ''
try{
await Auth.signIn(email, password)
}catch(err){
console.log(`Login Error [${err}]`)
if(err)
state.loginError = err.message || err
return
}
await dispatch('fetchUser')
},
async signup({commit, state}, {email, password}){
state.signupError = ''
try{
await Auth.signUp({
username: email,
email: email,
password: password,
attributes: {
email: email,
},
validationData: [],
})
//switch email confirmation form
commit('confirm', true)
}catch(err){
console.log(`Signup Error [${err}]`)
if(err)
state.signupError = err.message || err
commit('confirm', false)
}
},
async confirm({commit, dispatch, state}, {email, code}){
state.confirmError = ''
try{
await Auth.confirmSignUp(email, code, {
forceAliasCreation: true,
})
}catch(err){
console.log(`Confirm Error [${err}]`)
if(err)
state.confirmError = err.message || err
return
}
commit('confirm', false)
},
async authState({commit, dispatch}, state){
if(state === 'signedIn')
await dispatch('fetchUser')
else
commit('user', null)
},
async fetchUser({commit, dispatch}){
try{
const user = await Auth.currentAuthenticatedUser()
const expires = user.getSignInUserSession().getIdToken().payload.exp - Math.floor(new Date().getTime() / 1000)
console.log(`Token expires in ${expires} seconds`)
setTimeout(async () => {
console.log('Renewing Token')
await dispatch('fetchUser')
}, expires * 1000)
commit('user', user)
}catch(err){
commit('user', null)
}
},
async logout({commit}){
await Auth.signOut()
commit('user', null)
},
}
// Getters ====================
store.getters = {
}
// Export =====================
export default store
vue-customizing-amplify-authentication-vuex-module.js
When a user logs in, a timer is set to automatically refresh the token when it expires.
authorized
A boolean you can check if a user is authorized ($store.state.account.authorized
)user
An object of the user’s informationloginError
A string containing the Cognito response on an invalid login attemptsignupError
A string containing the Cognito response on an invalid signup attemptconfirm
Boolean indicating if the user wants to confirm their email address. This can happen right after signup or if a user comes back later.confirmError
A string containing the Cognito response on an invalid email confirmationThese 3 module actions trigger the AWS Amplify Authentication/Cognito Service.
// login a user
this.$store.dispatch('account/login', {email, password})
/*
signup a user -- after verifying your field rules:
* password minimum characters
* password strength policy
* email or username requirements
*/
this.$store.dispatch('account/signup', {email, password})
// Validate confirmation code sent to the user's email address
this.$store.dispatch('account/confirm', {email, code})
// Once confirmed take user to login form
vue-customizing-amplify-authentication-vuex-module-api.js
fetchUser
attempts to refresh the user
state from Cognito. Should only call once on application initialize, or if you want to refresh the tokenlogout
will ensure the user has logged outMost cases will require logic run when a user gets logged in or out. You can add this to main.js
or app.vue
then choose to redirect the user, load components, etc on change.
{
watch: {
'$store.state.account.authorized': async function(n, o){
if(n, o){
//logic for user logging in
await this.$api('user', 'signin') //example for logging in user to server
if(this.$route.path !== '/' || !o)
this.$router.push('/')
}else{
//logic for user logging out
if(this.$route.path !== '/')
this.$router.push('/')
}
},
},
// attempt to login user from session
async created(){
await store.dispatch('account/fetchUser')
},
}
vue-customizing-amplify-authentication-handling-authentication-changes.js
You may need to obtain the JWT
for interacting with a server or API. Here is some example code to fetch it:
// Dependencies ===============
import {Auth} from 'aws-amplify'
// Function ===================
async function getJWT(state){
if(state.account.authorized){
const session = await Auth.currentSession()
const jwt = session.idToken.jwtToken
}else{
//handle not logged in
}
}
vue-customizing-amplify-authentication-fetching-jwt.js
The state is passed to the method purely to check if the user is currently authorized, this can be handled differently.
Extending Authentication to a Server API
Very soon I will release an article on:
This lengthy example component handles all operations on one page (login, signup, and email validation).
This would be easy to customize for your Cognito setup and port to Bootstrap/Element/whatever framework you use.
<template><v-layout row wrap class="pt-4">
<!-- login -->
<v-flex md6 v-if="!confirm" class="pa-3">
<section title="Login to My Website">
<v-form @submit.prevent.stop="userLogin()" ref="form-login" autocomplete="off">
<v-text-field v-model="loginEmail" label="Email" required autocomplete="off" :rules="emailRules"></v-text-field>
<v-text-field v-model="loginPassword" type="password" label="Password" required autocomplete="off" :rules="passwordRules"></v-text-field>
<v-alert :value="$store.state.account.loginError" color="error" icon="far fa-exclamation-triangle" outline>{{ $store.state.account.loginError }}</v-alert>
<v-btn color="primary" type="submit">Login</v-btn>
<br><br>
<span @click="confirm = true">Need to <span style="text-decoration: underline; color: #2196F3; cursor: pointer;">confirm</span> your email address?</span>
</v-form>
</section>
</v-flex>
<!-- signup -->
<v-flex md6 v-if="!confirm" class="pa-3">
<section title="Signup to My Website">
<v-form @submit.prevent.stop="userSignup()" ref="form-signup" autocomplete="off">
<v-text-field v-model="signupEmail" label="Email address" required hint="We'll never share your email with anyone else." persistent-hint :rules="emailRules"></v-text-field>
<v-text-field v-model="signupPassword" type="password" label="Password" required browser-autocomplete="new-password" :rules="passwordRules"></v-text-field>
<v-text-field v-model="signupPasswordConfirm" type="password" label="Confirm Password" required browser-autocomplete="new-password" :rules="passwordRules"></v-text-field>
<v-alert :value="signupError || $store.state.account.signupError" color="error" icon="far fa-exclamation-triangle" outline>{{ signupError ? signupError : $store.state.account.signupError }}</v-alert>
<v-btn color="primary" type="submit">Signup</v-btn>
<br><br>
<v-alert :value="true" type="info" outline>You will be required to confirm your email by entering the code that has been emailed to you.</v-alert>
</v-form>
</section>
</v-flex>
<!-- confirm -->
<v-flex md2 style="padding: 5%;" v-if="confirm"/>
<v-flex md8 class="pa-3" v-if="confirm">
<section title="Confirm Your Email">
<v-form @submit.prevent.stop="userConfirm()" ref="form-confirm" autocomplete="off">
<v-text-field v-model="confirmEmail" label="Email address" required browser-autocomplete="off" :rules="emailRules"></v-text-field>
<v-text-field v-model="confirmConfirmation" type="text" browser-autocomplete="new-password" label="Confirmation Number" required hint="Check your email (and spam folder) for a confirmation Number." persistent-hint :rules="confirmationRules"></v-text-field>
<v-alert v-if="$store.state.account.confirmError" color="error" icon="far fa-exclamation-triangle" outline>{{ $store.state.account.confirmError }}</v-alert>
<v-btn color="primary" type="submit">Confirm</v-btn>
<br><br>
<v-alert :value="true" type="info" outline>You will need to login after confirming your email.</v-alert>
<br>
<span @click.stop="confirm = false" style="cursor: pointer;">Need to <span style="text-decoration: underline; color: #2196F3; cursor: pointer;">login or signup</span>?</span>
</v-form>
</section>
</v-flex>
<v-flex md2 style="padding: 5%;" v-if="confirm"/>
<v-progress-linear :indeterminate="true" color="secondary" v-if="loading"></v-progress-linear>
</v-layout></template>
<script>
// Dependencies ===============
import section from '@/components/section'
// Core =======================
let component = {
data: () => ({
loginEmail: '',
loginPassword: '',
confirmEmail: '',
confirmConfirmation: '',
signupEmail: '',
signupPassword: '',
signupPasswordConfirm: '',
signupError: '',
loading: false,
passwordRules: [
v => !!v || 'Password is required',
v => (v && v.length >= 8) || 'Password must be at least 8 characters',
],
emailRules: [
v => !!v || 'E-mail is required',
v => /.+@.+/.test(v) || 'E-mail must be valid',
],
confirmationRules: [
v => (v && v.length >= 5) || 'Verification code must be at least 5 numbers',
],
}),
computed: {
confirm: {
get(){
return this.$store.state.account.confirm
},
set(value){
this.$store.commit('account/confirm', value)
},
},
},
methods: {
async userLogin(){
if(!this.$refs['form-login'].validate())
return
this.loading = true
await this.$store.dispatch('account/login', {email: this.loginEmail, password: this.loginPassword})
this.loading = false
return false
},
async userSignup(){
if(!this.$refs['form-signup'].validate())
return
if(this.signupPassword !== this.signupPasswordConfirm){
this.signupError = 'Confirmation password must match'
return
}
this.signupError = ''
this.loading = true
this.confirmEmail = this.signupEmail
await this.$store.dispatch('account/signup', {email: this.signupEmail, password: this.signupPassword})
this.loading = false
return false
},
async userConfirm(){
if(!this.$refs['form-confirm'].validate())
return
this.loading = true
await this.$store.dispatch('account/confirm', {email: this.confirmEmail, code: this.confirmConfirmation})
this.loading = false
return false
},
noAutoComplete(){
this.$el.querySelectorAll('input[type="text"][autocomplete="off"').forEach(it => {
it.setAttribute('autocomplete', 'new-password')
})
},
},
components: {
section,
},
async created(){
this.confirm = this.$store.state.account.user && this.$store.state.account.user.attributes && !this.$store.state.account.user.attributes.email_verified
},
mounted(){
this.noAutoComplete()
},
}
// Export =====================
export default component
</script>
<style scoped>
button{
background-color: #14A3F2;
font-weight: 500;
letter-spacing: -.5px;
}
div.alert{
color: #990000;
}
</style>
vue-customizing-amplify-authentication-example-component.vue
section
component is a simple card view with a titleThe computed confirmation variable tells the sign-in component if the user wants to confirm their email address (with the verification code sent). This happens when the user has just signed up or when the user has returned from checking their email for the verification code. Once verified, the user can log in.
There are two levels of error handling:
The errors are then displayed on the components until a successful submittal.
That’s all for now, let me know if I am missing something or you want more information on anything.
✅ 30s ad
☞ Vue.js 2 Academy: Learn Vue Step by Step
☞ Vue.js 2 Essentials: Build Your First Vue App
☞ Vue JS 2: From Beginner to Professional (includes Vuex)
#vue-js #javascript #serverless