Snippets
Created by
Brandon Nielsen
last modified
Basic static page with authentication using a Cognito user pool
Requirements:
- Login page, either served via localhost for testing, or available via HTTPS
- Privacy policy page, available via HTTP/HTTPS
- Amazon / Google / Facebook authentication setup through respective page (https://developer.amazon.com for 'Login with Amazon' NOTE: this is NOT an AWS page, but Amazon Developer Services)
- Cognito user pool
- Cognito user pool app client, no app secret required for web auth
- Cognito user pool app client with "Authorization code grant" OAuth Flow allowed
- Login and logout callback URLs registered with Cognito user pool app client
"Authorization code grant" MUST be enabled in User pool -> App integration -> App client settings (Allowed OAuth flows), a request_error
will appear otherwise.
PKCE code verification failures result in a request_error
when authorizing with Amazon, instead of invalid_grant
as specified in the OAuth2 PKCE RFC.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | //Constants we need
const APP_BASE_URL = '<APP_BASE_URL>';
const AWS_COGNITO_POOL_CLIENT_ID = '<COGNITO_POOL_CLIENT_ID>';
const AWS_COGNITO_POOL_URI = '<COGNITO_POOL_URI>';
//The following must be registered with the Cognito app client
const AWS_COGNITO_LOGIN_CALLBACK_URI = APP_BASE_URL + '/?action=login';
const AWS_COGNITO_LOGOUT_CALLBACK_URI = APP_BASE_URL + '/?action=logout';
//API endpoints
const AWS_COGNITO_AUTHORIZE_ENDPOINT = AWS_COGNITO_POOL_URI + '/oauth2/authorize';
const AWS_COGNITO_CODE_EXCHANGE_ENDPOINT = AWS_COGNITO_POOL_URI + '/oauth2/token';
const AWS_COGNITO_USER_INFO_ENDPOINT = AWS_COGNITO_POOL_URI + '/oauth2/userInfo';
const AWS_COGNITO_LOGOUT_ENDPOINT = AWS_COGNITO_POOL_URI + '/logout?client_id=' + AWS_COGNITO_POOL_CLIENT_ID + '&logout_uri=' + AWS_COGNITO_LOGOUT_CALLBACK_URI;
//Somewhere to store auth tokens, we don't use cookies or local storage because we have no way of knowing if the user has selected 'remember me' or not, so we only 'remember' for the length of a session
const sessionStorage = window.sessionStorage;
//Setup the UI
document.getElementById('hrefLogin').onclick = loginClickFunction;
document.getElementById('hrefLogout').onclick = logoutClickFunction;
document.getElementById('hrefLogout').style.display = 'none';
//Parse the URL params
const urlParams = new URL(window.location.href).searchParams;
const action = urlParams.get('action');
if (action === 'login') {
if (sessionStorage.getItem('cognito_login_state') && sessionStorage.getItem('cognito_login_state') === urlParams.get('state') && sessionStorage.getItem('cognito_pkce_code_verifier')) {
//Clear the state variable, it shouldn't be reused
sessionStorage.removeItem('cognito_login_state');
//Exchange the auth token for an access token, get the current user
const authCode = urlParams.get('code');
exchangeAuthToken(authCode, sessionStorage.getItem('cognito_pkce_code_verifier'), tokenSuccessCallback, errorCallback);
}
}
else if (action === 'logout') {
clearAuthData();
//Redirect to base URL
window.location.href = APP_BASE_URL;
}
else if (sessionStorage.getItem('cognito_expiration') && sessionStorage.getItem('cognito_access_token')) {
if (Math.floor(Date.now() / 1000) < sessionStorage.getItem('cognito_expiration')) {
//We have a current token
getCurrentUser(sessionStorage.getItem('cognito_access_token'), getUserSuccessCallback, errorCallback);
}
else {
//Token is expired, clean up now defunct auth data
clearAuthData();
}
}
//Setup async event callbacks
function tokenSuccessCallback(tokenData) {
//Save the data
sessionStorage.setItem('cognito_access_token', tokenData['access_token']);
sessionStorage.setItem('cognito_id_token', tokenData['id_token']);
sessionStorage.setItem('cognito_refresh_token', tokenData['refresh_token']);
sessionStorage.setItem('cognito_expiration', Math.floor(Date.now() / 1000) + tokenData['expires_in']);
//Don't reuse PKCE token
sessionStorage.removeItem('cognito_pkce_code_verifier');
//Redirect to base URL, the user info request will be made there
window.location.href = APP_BASE_URL;
}
function getUserSuccessCallback(userData) {
//Hide the login link
document.getElementById('hrefLogin').style.display = 'none';
//Show the logout link
document.getElementById('hrefLogout').style.display = 'inline';
console.log(userData);
}
function errorCallback(errorData) {
console.log(errorData);
}
function loginClickFunction() {
//Generate a 'state' variable we can use to make sure we're returning from an authentication flow we started, this adds no security, it's purely for flow validation
const stateParameter = generateSecret();
//Store the state so we can verify it later
sessionStorage.setItem('cognito_login_state', stateParameter);
//Generate a PKCE code verifier, again, since this is a public client, it provides no security, use a hash because it's easy, and satisifes length requirements
//From RFC 7636, the code verifier is base64url-encoded ASCII
const codeVerifier = base64UrlEncode(generateSecret());
sessionStorage.setItem('cognito_pkce_code_verifier', codeVerifier);
//The challenge, by definition, is base64 encoded SHA256 hash of the code verifier
const codeChallenge = hexToBase64UrlEncode(sha256(codeVerifier));
//Build the request query
const loginRequestPairs = [];
loginRequestPairs.push(encodeURIComponent('client_id') + '=' + encodeURIComponent(AWS_COGNITO_POOL_CLIENT_ID));
loginRequestPairs.push(encodeURIComponent('redirect_uri') + '=' + encodeURIComponent(AWS_COGNITO_LOGIN_CALLBACK_URI));
loginRequestPairs.push(encodeURIComponent('response_type') + '=' + encodeURIComponent('code'));
loginRequestPairs.push(encodeURIComponent('code_challenge') + '=' + encodeURIComponent(codeChallenge));
loginRequestPairs.push(encodeURIComponent('code_challenge_method') + '=' + encodeURIComponent('S256'));
loginRequestPairs.push(encodeURIComponent('state') + '=' + encodeURIComponent(stateParameter));
const loginRequestQuery = loginRequestPairs.join('&').replace(/%20/g, '+');
//Redirect to login
window.location.href = AWS_COGNITO_AUTHORIZE_ENDPOINT + '?' + loginRequestQuery;
return false;
}
function logoutClickFunction() {
//Redirect to logout
window.location.href = AWS_COGNITO_LOGOUT_ENDPOINT;
return false;
}
function exchangeAuthToken(authorizationCode, codeVerifier, successCallback, errorCallback) {
//Exchanges an authorization code for a set of user tokens
//https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html
const tokenRequest = new XMLHttpRequest();
//Build the form data
const tokenRequestPairs = [];
tokenRequestPairs.push(encodeURIComponent('grant_type') + '=' + encodeURIComponent('authorization_code'));
tokenRequestPairs.push(encodeURIComponent('client_id') + '=' + encodeURIComponent(AWS_COGNITO_POOL_CLIENT_ID));
tokenRequestPairs.push(encodeURIComponent('redirect_uri') + '=' + encodeURIComponent(AWS_COGNITO_LOGIN_CALLBACK_URI));
tokenRequestPairs.push(encodeURIComponent('code_verifier') + '=' + encodeURIComponent(codeVerifier));
tokenRequestPairs.push(encodeURIComponent('code') + '=' + encodeURIComponent(authorizationCode));
const tokenRequestData = tokenRequestPairs.join('&').replace(/%20/g, '+');
//Set up event handler
tokenRequest.onreadystatechange = function() {
if (tokenRequest.readyState == 4) {
if (tokenRequest.status == 200) {
successCallback(JSON.parse(tokenRequest.responseText));
}
else {
errorCallback(JSON.parse(tokenRequest.responseText));
}
}
};
//Submit the form
tokenRequest.open('POST', AWS_COGNITO_CODE_EXCHANGE_ENDPOINT, true);
tokenRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
tokenRequest.send(tokenRequestData);
}
function getCurrentUser(accessToken, successCallback, errorCallback) {
//Gets the user info
//https://docs.aws.amazon.com/cognito/latest/developerguide/userinfo-endpoint.html
const userInfoRequest = new XMLHttpRequest();
//Setup handler
userInfoRequest.onreadystatechange = function() {
if (userInfoRequest.readyState == 4) {
if (userInfoRequest.status == 200) {
successCallback(JSON.parse(userInfoRequest.responseText));
}
else {
errorCallback(JSON.parse(tokenRequest.responseText));
}
}
};
userInfoRequest.open('GET', AWS_COGNITO_USER_INFO_ENDPOINT, true);
userInfoRequest.setRequestHeader('Authorization', 'Bearer ' + accessToken);
userInfoRequest.send(null);
}
function clearAuthData() {
sessionStorage.removeItem('cognito_access_token');
sessionStorage.removeItem('cognito_id_token');
sessionStorage.removeItem('cognito_refresh_token');
sessionStorage.removeItem('cognito_expiration');
sessionStorage.removeItem('cognito_login_state');
sessionStorage.removeItem('cognito_pkce_code_verifier');
}
function base64UrlEncode(toEncode) {
var encodedString = btoa(toEncode);
encodedString = encodedString.replace(/=+$/, '');
encodedString = encodedString.replace(/\+/g, '-');
encodedString = encodedString.replace(/\//g, '_');
return encodedString;
}
function hexToBase64UrlEncode(hexString) {
//https://stackoverflow.com/a/30614192
hexString = hexString.replace(/\r|\n/g, '');
hexString = hexString.replace(/([\da-fA-F]{2}) ?/g, '0x$1 ');
hexString = hexString.replace(/ +$/, '');
const hexArray = hexString.split(' ');
const byteString = String.fromCharCode.apply(null, hexArray);
return base64UrlEncode(byteString);
}
function generateSecret() {
//Generates a cryptographically secure "secret" with 256 bits of entropy, returned as a hex string
return sha256(window.crypto.getRandomValues(new Uint32Array(8)));
}
|
Comments (0)
You can clone a snippet to your computer for local editing. Learn more.