This week the team will be talking about Decentralized Authentication, in particular, Amazon Cognito
1 Place for your credentials
- SAML (Security Assertion Markup Language): Single point of logging into any application. An example is the faceid which you use the face to log in instead of your credential. this is used for authentication
- OpenID Connect: allows you to connect using your social media credential (using google credential, LinkedIn Facebook etc.) rather than creating a new username and password. this is used for authentication
- OAuth: Use for authorization
What is Decentralised Authentication? Decentralised authentication is an extension of the concept of services above. If you want to compare is some sort of password manager that you can use in different application
Amazon Cognito is an aws service that allows users to authenticate. credentials are stored in cognito. it is similar of a directory under the aws hood.
Amazon Cognito User Pool
Cognito Identity Pool
Reason for using Amazon Cognito
- User directory for Customer
- Ability to access aws resources for the application being built
- Identity broker for AWS Resources with temporary credentials
- It can extend users to AWS Resources easily.
This Week Cirag did not post any video about cost so I did some research this is what I found:
The Cognito Your User Pool feature has a free tier of 50,000 MAUs ( monthly active users) per account for users who sign in directly to Cognito User Pools and 50 MAUs for users federated through SAML 2.0 based identity providers. The free tier does not automatically expire at the end of your 12 month AWS Free Tier term, and it is available to both existing and new AWS customers indefinitely. Please note - the free tier pricing isn’t available for both Your User Pool feature and SAML or OIDC federation in the AWS GovCloud regions.
For reference click Here.
Please click here to create the user pool using the console
Using the terminal go to the dictory by typing the following command:
cd front-react-js
npm i aws-amplyfy --save
this command will install amplify library and will be added to the package.json
from the app.js, add the following codes:
import { Amplify } from 'aws-amplify';
Amplify.configure({
"AWS_PROJECT_REGION": process.env.REACT_APP_AWS_PROJECT_REGION,
//"aws_cognito_identity_pool_id": process.env.REACT_APP_AWS_COGNITO_IDENTITY_POOL_ID,
"aws_cognito_region": process.env.REACT_APP_AWS_COGNITO_REGION,
"aws_user_pools_id": process.env.REACT_APP_AWS_USER_POOLS_ID,
"aws_user_pools_web_client_id": process.env.REACT_APP_CLIENT_ID,
"oauth": {},
Auth: {
// We are not using an Identity Pool
// identityPoolId: process.env.REACT_APP_IDENTITY_POOL_ID, // REQUIRED - Amazon Cognito Identity Pool ID
region: process.env.REACT_APP_AWS_PROJECT_REGION, // REQUIRED - Amazon Cognito Region
userPoolId: process.env.REACT_APP_AWS_USER_POOLS_ID, // OPTIONAL - Amazon Cognito User Pool ID
userPoolWebClientId: process.env.REACT_APP_AWS_USER_POOLS_WEB_CLIENT_ID, // OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string)
}
});
from the docker-compose.yml on the frontend-react-js under environment:, add the following code
REACT_APP_AWS_PROJECT_REGION: "${AWS_DEFAULT_REGION}"
REACT_APP_AWS_COGNITO_REGION: "${AWS_DEFAULT_REGION}"
REACT_APP_AWS_USER_POOLS_ID: "${AWS_USER_POOLS_ID}"
REACT_APP_CLIENT_ID: "${APP_CLIENT_ID}"
Make sure to create the env var AWS_USER_POOLS_ID and APP_CLIENT_ID on gitpod and codespace. (N.B: Since these env vars have not been loaded during the booting, you might get an error. either you rebuild your workspace or you pass the variable via the terminal. I do not hardcode the env vars for security reasons and for simplicity) The AWS_USER_POOLS_ID and APP_CLIENT_ID you find when you configure the cognito user pool.
from the homefeedpage.js insert the following command
import { Auth } from 'aws-amplify';
this instruction is already implemented so you can skip this part
const [user, setUser] = React.useState(null);
delete the code with the cookies
const checkAuth = async () => {
console.log('checkAuth')
// [TODO] Authenication
if (Cookies.get('user.logged_in')) {
display_name: Cookies.get('user.name'),
handle: Cookies.get('user.username')
}
};
and replace with the following that used cognito
// check if we are authenicated
const checkAuth = async () => {
Auth.currentAuthenticatedUser({
// Optional, By default is false.
// If set to true, this call will send a
// request to Cognito to get the latest user data
bypassCache: false
})
.then((user) => {
console.log('user',user);
return Auth.currentAuthenticatedUser()
}).then((cognito_user) => {
setUser({
display_name: cognito_user.attributes.name,
handle: cognito_user.attributes.preferred_username
})
})
.catch((err) => console.log(err));
};
this instruction is already implemented so you can skip this part
// check when the page loads if we are authenicated
React.useEffect(()=>{
loadData();
checkAuth();
}, [])
This instruction is already implemented so you can skip this part as well.
<DesktopNavigation user={user} active={'home'} setPopped={setPopped} />
<DesktopSidebar user={user} />
On profileinfo.js, delete the following code
import Cookies from 'js-cookie'
and replace with the following
import { Auth } from 'aws-amplify';
remove the following code
console.log('signOut')
// [TODO] Authenication
Cookies.remove('user.logged_in')
//Cookies.remove('user.name')
//Cookies.remove('user.username')
//Cookies.remove('user.email')
//Cookies.remove('user.password')
//Cookies.remove('user.confirmation_code')
window.location.href = "/"
and we replace with the new signout
const signOut = async () => {
try {
await Auth.signOut({ global: true });
window.location.href = "/"
} catch (error) {
console.log('error signing out: ', error);
}
From the signinpage.js remove the following code
import Cookies from 'js-cookie'
and replace with the following
import { Auth } from 'aws-amplify';
remove the following code
const onsubmit = async (event) => {
event.preventDefault();
setErrors('')
console.log('onsubmit')
if (Cookies.get('user.email') === email && Cookies.get('user.password') === password){
Cookies.set('user.logged_in', true)
window.location.href = "/"
} else {
setErrors("Email and password is incorrect or account doesn't exist")
}
return false
}
and replace it with the new one
const onsubmit = async (event) => {
setErrors('')
event.preventDefault();
Auth.signIn(email, password)
.then(user => {
console.log('user',user)
localStorage.setItem("access_token", user.signInUserSession.accessToken.jwtToken)
window.location.href = "/"
})
.catch(error => {
if (error.code == 'UserNotConfirmedException') {
window.location.href = "/confirm"
}
setErrors(error.message)
});
return false
}
To try, just launch the container up on "docker-compose.yml" and see if the login page works. to troubleshoot open "developer tools" or use inspect (browser) if you receive "NotAuthorizedException: Incorrect user or password".This means everything is set properly. if you got an error "auth not defined", the problem is the cognito user pool configuration. need to recreate.
Create a user on the cognito user pool and force change the password using the command on troubleshooting (there is no way to change on password via console). the password to login will be Testing1234! (as the commandline shows)
Since you have managed to access using the credential created via console, it is time to delete it cos it is no anymore needed.
From the signuppage.js remove the following code
import Cookies from 'js-cookie'
and replace with the following
import { Auth } from 'aws-amplify';
delete the following command
const onsubmit = async (event) => {
event.preventDefault();
console.log('SignupPage.onsubmit')
// [TODO] Authenication
Cookies.set('user.name', name)
Cookies.set('user.username', username)
Cookies.set('user.email', email)
Cookies.set('user.password', password)
Cookies.set('user.confirmation_code',1234)
window.location.href = `/confirm?email=${email}`
return false
}
and add the new code
const onsubmit = async (event) => {
event.preventDefault();
setErrors('')
try {
const { user } = await Auth.signUp({
username: email,
password: password,
attributes: {
name: name,
email: email,
preferred_username: username,
},
autoSignIn: { // optional - enables auto sign in after user is confirmed
enabled: true,
}
}) ;
console.log(user);
window.location.href = `/confirm?email=${email}`
} catch (error) {
console.log(error);
setErrors(error.message)
}
return false
}
from the confirmationpage.js, remove the following code
import Cookies from 'js-cookie'
and replace with the following
import { Auth } from 'aws-amplify';
and remove the following code
const resend_code = async (event) => {
console.log('resend_code')
// [TODO] Authenication
}
and replace with the following
const resend_code = async (event) => {
setErrors('')
try {
await Auth.resendSignUp(email);
console.log('code resent successfully');
setCodeSent(true)
} catch (err) {
// does not return a code
// does cognito always return english
// for this to be an okay match?
console.log(err)
if (err.message == 'Username cannot be empty'){
setCognitoErrors("You need to provide an email in order to send Resend Activiation Code")
} else if (err.message == "Username/client id combination not found."){
setCognitoErrors("Email is invalid or cannot be found.")
}
}
}
and remove the following code
const onsubmit = async (event) => {
event.preventDefault();
console.log('ConfirmationPage.onsubmit')
// [TODO] Authenication
if (Cookies.get('user.email') === undefined || Cookies.get('user.email') === '' || Cookies.get('user.email') === null){
setErrors("You need to provide an email in order to send Resend Activiation Code")
} else {
if (Cookies.get('user.email') === email){
if (Cookies.get('user.confirmation_code') === code){
Cookies.set('user.logged_in',true)
window.location.href = "/"
} else {
setErrors("Code is not valid")
}
} else {
setErrors("Email is invalid or cannot be found.")
}
}
return false
}
and replace with the cognito code
const onsubmit = async (event) => {
event.preventDefault();
setCognitoErrors('')
try {
await Auth.confirmSignUp(email, code);
window.location.href = "/"
} catch (error) {
setCognitoErrors(error.message)
}
return false
}
from the recoverpage.js, add the following code
import { Auth } from 'aws-amplify';
remove the following code
const onsubmit_send_code = async (event) => {
event.preventDefault();
console.log('onsubmit_send_code')
return false
}
and add the these lines
const onsubmit_send_code = async (event) => {
event.preventDefault();
setErrors('')
Auth.forgotPassword(username)
.then((data) => setFormState('confirm_code') )
.catch((err) => setErrors(err.message) );
return false
}
remove the following code
const onsubmit_confirm_code = async (event) => {
event.preventDefault();
console.log('onsubmit_confirm_code')
return false
}
with the following new code
const onsubmit_confirm_code = async (event) => {
event.preventDefault();
setCognitoErrors('')
if (password == passwordAgain){
Auth.forgotPasswordSubmit(username, code, password)
.then((data) => setFormState('success'))
.catch((err) => setCognitoErrors(err.message) );
} else {
setCognitoErrors('Passwords do not match')
}
return false
}
In our application, there are 2 user experience problems:
- During the confirmation, user needs to write the email manually. this could cause possible human error.
- After the registration, user gets redirected to the home page but not signed in yet. this could create confusion and user can create a new account accidentally.
The solution is to store the value using localstorage (many thanks to Abdassalam Hashnode) and use this across other pages.
The changes will be between the signuppage.js, confirmationpage.js and signinpage.js
From the signup page, add the following code this will store the email to the local storage
// SignupPage.js
const onsubmit = async (event) => {
// ...
try {
// ...
// Store email in local storage to use it in confirmation & sign-in page
localStorage.setItem('email', email);
// redirect user to confirmation page after signing up
window.location.href = `/confirm`
}
// ...
from the confirmation page, add the following code. this checks if the local storage contains email
// ConfirmationPage.js
// ...
// Get email from the signup page where we stored the email in localStorage
React.useEffect(() => {
const storedEmail = localStorage.getItem('email');
// check if the email is set, if it's not set then we will ignore it, and use the vlaue we type in the email box
if (storedEmail) {
// Filling the Email
setEmail(storedEmail);
}
}, []);
const onsubmit = async (event) => {
// ...
from the signup page, add the following code. this set the email from the local storage to the confirmation page
// SigninPage.js
// Get email from the signup page where we stored the email in localStorage
React.useEffect(() => {
const storedEmail = localStorage.getItem('email');
if (storedEmail) {
setEmail(storedEmail);
// Remove the email from local storage because we're done with it.
localStorage.removeItem('email');
}
}, []);
To redirect the home page already logged in, insert the following code
// ConfirmationPage.js
const onsubmit = async (event) => {
// ...
try {
await Auth.confirmSignUp(email, code);
// Redirect user to sign-in page instead of home page.
window.location.href = "/signin"
// ...
}
aws cognito-idp admin-set-user-password --username nameofusername --password Testing1234! --user-pool-id "${AWS_USER_POOLS_ID}" --permanent
Reference