UNPKG

@idea-ionic/auth

Version:
1,770 lines (1,767 loc) 197 kB
import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { inject, Injectable, Component, EventEmitter, Output, Input } from '@angular/core'; import * as i1 from '@angular/forms'; import { FormsModule } from '@angular/forms'; import { NavController, IonButton, IonText, IonCol, IonRow, IonIcon, IonLabel, IonCardSubtitle, IonCardTitle, IonCardHeader, IonImg, IonCardContent, IonCard, IonContent, IonItem, IonInput, IonCheckbox, PopoverController, ModalController, IonListHeader, IonList, IonTitle, IonButtons, IonToolbar, IonHeader } from '@ionic/angular/standalone'; import { Browser } from '@capacitor/browser'; import { isEmpty } from 'idea-toolbox'; import { IDEAEnvironment, IDEATranslationsService, IDEAStorageService, IDEAMessageService, IDEALoadingService, IDEATranslatePipe } from '@idea-ionic/common'; import { CognitoUserPool, AuthenticationDetails, CognitoUser, CognitoUserAttribute, CognitoRefreshToken } from 'amazon-cognito-identity-js'; import { ActivatedRoute, Router } from '@angular/router'; import { toCanvas } from 'qrcode'; const COMMON_PASSWORDS_TOP_1000 = [ '123456', 'password', '12345678', 'qwerty', '123456789', '12345', '1234', '111111', '1234567', 'dragon', '123123', 'baseball', 'abc123', 'football', 'monkey', 'letmein', '696969', 'shadow', 'master', '666666', 'qwertyuiop', '123321', 'mustang', '1234567890', 'michael', '654321', 'pussy', 'superman', '1qaz2wsx', '7777777', 'fuckyou', '121212', '000000', 'qazwsx', '123qwe', 'killer', 'trustno1', 'jordan', 'jennifer', 'zxcvbnm', 'asdfgh', 'hunter', 'buster', 'soccer', 'harley', 'batman', 'andrew', 'tigger', 'sunshine', 'iloveyou', 'fuckme', '2000', 'charlie', 'robert', 'thomas', 'hockey', 'ranger', 'daniel', 'starwars', 'klaster', '112233', 'george', 'asshole', 'computer', 'michelle', 'jessica', 'pepper', '1111', 'zxcvbn', '555555', '11111111', '131313', 'freedom', '777777', 'pass', 'fuck', 'maggie', '159753', 'aaaaaa', 'ginger', 'princess', 'joshua', 'cheese', 'amanda', 'summer', 'love', 'ashley', '6969', 'nicole', 'chelsea', 'biteme', 'matthew', 'access', 'yankees', '987654321', 'dallas', 'austin', 'thunder', 'taylor', 'matrix', 'william', 'corvette', 'hello', 'martin', 'heather', 'secret', 'fucker', 'merlin', 'diamond', '1234qwer', 'gfhjkm', 'hammer', 'silver', '222222', '88888888', 'anthony', 'justin', 'test', 'bailey', 'q1w2e3r4t5', 'patrick', 'internet', 'scooter', 'orange', '11111', 'golfer', 'cookie', 'richard', 'samantha', 'bigdog', 'guitar', 'jackson', 'whatever', 'mickey', 'chicken', 'sparky', 'snoopy', 'maverick', 'phoenix', 'camaro', 'sexy', 'peanut', 'morgan', 'welcome', 'falcon', 'cowboy', 'ferrari', 'samsung', 'andrea', 'smokey', 'steelers', 'joseph', 'mercedes', 'dakota', 'arsenal', 'eagles', 'melissa', 'boomer', 'booboo', 'spider', 'nascar', 'monster', 'tigers', 'yellow', 'xxxxxx', '123123123', 'gateway', 'marina', 'diablo', 'bulldog', 'qwer1234', 'compaq', 'purple', 'hardcore', 'banana', 'junior', 'hannah', '123654', 'porsche', 'lakers', 'iceman', 'money', 'cowboys', '987654', 'london', 'tennis', '999999', 'ncc1701', 'coffee', 'scooby', '0000', 'miller', 'boston', 'q1w2e3r4', 'fuckoff', 'brandon', 'yamaha', 'chester', 'mother', 'forever', 'johnny', 'edward', '333333', 'oliver', 'redsox', 'player', 'nikita', 'knight', 'fender', 'barney', 'midnight', 'please', 'brandy', 'chicago', 'badboy', 'iwantu', 'slayer', 'rangers', 'charles', 'angel', 'flower', 'bigdaddy', 'rabbit', 'wizard', 'bigdick', 'jasper', 'enter', 'rachel', 'chris', 'steven', 'winner', 'adidas', 'victoria', 'natasha', '1q2w3e4r', 'jasmine', 'winter', 'prince', 'panties', 'marine', 'ghbdtn', 'fishing', 'cocacola', 'casper', 'james', '232323', 'raiders', '888888', 'marlboro', 'gandalf', 'asdfasdf', 'crystal', '87654321', '12344321', 'sexsex', 'golden', 'blowme', 'bigtits', '8675309', 'panther', 'lauren', 'angela', 'bitch', 'spanky', 'thx1138', 'angels', 'madison', 'winston', 'shannon', 'mike', 'toyota', 'blowjob', 'jordan23', 'canada', 'sophie', 'Password', 'apples', 'dick', 'tiger', 'razz', '123abc', 'pokemon', 'qazxsw', '55555', 'qwaszx', 'muffin', 'johnson', 'murphy', 'cooper', 'jonathan', 'liverpoo', 'david', 'danielle', '159357', 'jackie', '1990', '123456a', '789456', 'turtle', 'horny', 'abcd1234', 'scorpion', 'qazwsxedc', '101010', 'butter', 'carlos', 'password1', 'dennis', 'slipknot', 'qwerty123', 'booger', 'asdf', '1991', 'black', 'startrek', '12341234', 'cameron', 'newyork', 'rainbow', 'nathan', 'john', '1992', 'rocket', 'viking', 'redskins', 'butthead', 'asdfghjkl', '1212', 'sierra', 'peaches', 'gemini', 'doctor', 'wilson', 'sandra', 'helpme', 'qwertyui', 'victor', 'florida', 'dolphin', 'pookie', 'captain', 'tucker', 'blue', 'liverpool', 'theman', 'bandit', 'dolphins', 'maddog', 'packers', 'jaguar', 'lovers', 'nicholas', 'united', 'tiffany', 'maxwell', 'zzzzzz', 'nirvana', 'jeremy', 'suckit', 'stupid', 'porn', 'monica', 'elephant', 'giants', 'jackass', 'hotdog', 'rosebud', 'success', 'debbie', 'mountain', '444444', 'xxxxxxxx', 'warrior', '1q2w3e4r5t', 'q1w2e3', '123456q', 'albert', 'metallic', 'lucky', 'azerty', '7777', 'shithead', 'alex', 'bond007', 'alexis', '1111111', 'samson', '5150', 'willie', 'scorpio', 'bonnie', 'gators', 'benjamin', 'voodoo', 'driver', 'dexter', '2112', 'jason', 'calvin', 'freddy', '212121', 'creative', '12345a', 'sydney', 'rush2112', '1989', 'asdfghjk', 'red123', 'bubba', '4815162342', 'passw0rd', 'trouble', 'gunner', 'happy', 'fucking', 'gordon', 'legend', 'jessie', 'stella', 'qwert', 'eminem', 'arthur', 'apple', 'nissan', 'bullshit', 'bear', 'america', '1qazxsw2', 'nothing', 'parker', '4444', 'rebecca', 'qweqwe', 'garfield', '01012011', 'beavis', '69696969', 'jack', 'asdasd', 'december', '2222', '102030', '252525', '11223344', 'magic', 'apollo', 'skippy', '315475', 'girls', 'kitten', 'golf', 'copper', 'braves', 'shelby', 'godzilla', 'beaver', 'fred', 'tomcat', 'august', 'buddy', 'airborne', '1993', '1988', 'lifehack', 'qqqqqq', 'brooklyn', 'animal', 'platinum', 'phantom', 'online', 'xavier', 'darkness', 'blink182', 'power', 'fish', 'green', '789456123', 'voyager', 'police', 'travis', '12qwaszx', 'heaven', 'snowball', 'lover', 'abcdef', '00000', 'pakistan', '007007', 'walter', 'playboy', 'blazer', 'cricket', 'sniper', 'hooters', 'donkey', 'willow', 'loveme', 'saturn', 'therock', 'redwings', 'bigboy', 'pumpkin', 'trinity', 'williams', 'tits', 'nintendo', 'digital', 'destiny', 'topgun', 'runner', 'marvin', 'guinness', 'chance', 'bubbles', 'testing', 'fire', 'november', 'minecraft', 'asdf1234', 'lasvegas', 'sergey', 'broncos', 'cartman', 'private', 'celtic', 'birdie', 'little', 'cassie', 'babygirl', 'donald', 'beatles', '1313', 'dickhead', 'family', '12121212', 'school', 'louise', 'gabriel', 'eclipse', 'fluffy', '147258369', 'lol123', 'explorer', 'beer', 'nelson', 'flyers', 'spencer', 'scott', 'lovely', 'gibson', 'doggie', 'cherry', 'andrey', 'snickers', 'buffalo', 'pantera', 'metallica', 'member', 'carter', 'qwertyu', 'peter', 'alexande', 'steve', 'bronco', 'paradise', 'goober', '5555', 'samuel', 'montana', 'mexico', 'dreams', 'michigan', 'cock', 'carolina', 'yankee', 'friends', 'magnum', 'surfer', 'poopoo', 'maximus', 'genius', 'cool', 'vampire', 'lacrosse', 'asd123', 'aaaa', 'christin', 'kimberly', 'speedy', 'sharon', 'carmen', '111222', 'kristina', 'sammy', 'racing', 'ou812', 'sabrina', 'horses', '0987654321', 'qwerty1', 'pimpin', 'baby', 'stalker', 'enigma', '147147', 'star', 'poohbear', 'boobies', '147258', 'simple', 'bollocks', '12345q', 'marcus', 'brian', '1987', 'qweasdzxc', 'drowssap', 'hahaha', 'caroline', 'barbara', 'dave', 'viper', 'drummer', 'action', 'einstein', 'bitches', 'genesis', 'hello1', 'scotty', 'friend', 'forest', '010203', 'hotrod', 'google', 'vanessa', 'spitfire', 'badger', 'maryjane', 'friday', 'alaska', '1232323q', 'tester', 'jester', 'jake', 'champion', 'billy', '147852', 'rock', 'hawaii', 'badass', 'chevy', '420420', 'walker', 'stephen', 'eagle1', 'bill', '1986', 'october', 'gregory', 'svetlana', 'pamela', '1984', 'music', 'shorty', 'westside', 'stanley', 'diesel', 'courtney', '242424', 'kevin', 'porno', 'hitman', 'boobs', 'mark', '12345qwert', 'reddog', 'frank', 'qwe123', 'popcorn', 'patricia', 'aaaaaaaa', '1969', 'teresa', 'mozart', 'buddha', 'anderson', 'paul', 'melanie', 'abcdefg', 'security', 'lucky1', 'lizard', 'denise', '3333', 'a12345', '123789', 'ruslan', 'stargate', 'simpsons', 'scarface', 'eagle', '123456789a', 'thumper', 'olivia', 'naruto', '1234554321', 'general', 'cherokee', 'a123456', 'vincent', 'Usuckballz1', 'spooky', 'qweasd', 'cumshot', 'free', 'frankie', 'douglas', 'death', '1980', 'loveyou', 'kitty', 'kelly', 'veronica', 'suzuki', 'semperfi', 'penguin', 'mercury', 'liberty', 'spirit', 'scotland', 'natalie', 'marley', 'vikings', 'system', 'sucker', 'king', 'allison', 'marshall', '1979', '098765', 'qwerty12', 'hummer', 'adrian', '1985', 'vfhbyf', 'sandman', 'rocky', 'leslie', 'antonio', '98765432', '4321', 'softball', 'passion', 'mnbvcxz', 'bastard', 'passport', 'horney', 'rascal', 'howard', 'franklin', 'bigred', 'assman', 'alexander', 'homer', 'redrum', 'jupiter', 'claudia', '55555555', '141414', 'zaq12wsx', 'shit', 'patches', 'nigger', 'cunt', 'raider', 'infinity', 'andre', '54321', 'galore', 'college', 'russia', 'kawasaki', 'bishop', '77777777', 'vladimir', 'money1', 'freeuser', 'wildcats', 'francis', 'disney', 'budlight', 'brittany', '1994', '00000000', 'sweet', 'oksana', 'honda', 'domino', 'bulldogs', 'brutus', 'swordfis', 'norman', 'monday', 'jimmy', 'ironman', 'ford', 'fantasy', '9999', '7654321', 'PASSWORD', 'hentai', 'duncan', 'cougar', '1977', 'jeffrey', 'house', 'dancer', 'brooke', 'timothy', 'super', 'marines', 'justice', 'digger', 'connor', 'patriots', 'karina', '202020', 'molly', 'everton', 'tinker', 'alicia', 'rasdzv3', 'poop', 'pearljam', 'stinky', 'naughty', 'colorado', '123123a', 'water', 'test123', 'ncc1701d', 'motorola', 'ireland', 'asdfg', 'slut', 'matt', 'houston', 'boogie', 'zombie', 'accord', 'vision', 'bradley', 'reggie', 'kermit', 'froggy', 'ducati', 'avalon', '6666', '9379992', 'sarah', 'saints', 'logitech', 'chopper', '852456', 'simpson', 'madonna', 'juventus', 'claire', '159951', 'zachary', 'yfnfif', 'wolverin', 'warcraft', 'hello123', 'extreme', 'penis', 'peekaboo', 'fireman', 'eugene', 'brenda', '123654789', 'russell', 'panthers', 'georgia', 'smith', 'skyline', 'jesus', 'elizabet', 'spiderma', 'smooth', 'pirate', 'empire', 'bullet', '8888', 'virginia', 'valentin', 'psycho', 'predator', 'arizona', '134679', 'mitchell', 'alyssa', 'vegeta', 'titanic', 'christ', 'goblue', 'fylhtq', 'wolf', 'mmmmmm', 'kirill', 'indian', 'hiphop', 'baxter', 'awesome', 'people', 'danger', 'roland', 'mookie', '741852963', '1111111111', 'dreamer', 'bambam', 'arnold', '1981', 'skipper', 'serega', 'rolltide', 'elvis', 'changeme', 'simon', '1q2w3e', 'lovelove', 'fktrcfylh', 'denver', 'tommy', 'mine', 'loverboy', 'hobbes', 'happy1', 'alison', 'nemesis', 'chevelle', 'cardinal', 'burton', 'wanker', 'picard', '151515', 'tweety', 'michael1', '147852369', '12312', 'xxxx', 'windows', 'turkey', '456789', '1974', 'vfrcbv', 'sublime', '1975', 'galina', 'bobby', 'newport', 'manutd', 'daddy', 'american', 'alexandr', '1966', 'victory', 'rooster', 'qqq111', 'madmax', 'electric', 'bigcock', 'a1b2c3', 'wolfpack', 'spring', 'phpbb', 'lalala', 'suckme', 'spiderman', 'eric', 'darkside', 'classic', 'raptor', '123456789q', 'hendrix', '1982', 'wombat', 'avatar', 'alpha', 'zxc123', 'crazy', 'hard', 'england', 'brazil', '1978', '01011980', 'wildcat', 'polina', 'freepass' ]; /** * Cognito wrapper to manage the authentication flow. * * Note: in IDEA's Cognito users pools, the email is an alias of the username. */ class IDEAAuthService { constructor() { this._env = inject(IDEAEnvironment); this._translate = inject(IDEATranslationsService); this._storage = inject(IDEAStorageService); this.deviceKeyAttribute = 'custom:'.concat(this._env.idea.project); this.userPool = new CognitoUserPool({ UserPoolId: this._env.aws.cognito.userPoolId, ClientId: this._env.aws.cognito.userPoolClientId }); this.mfaProjectName = this._env.idea.auth.title || this._env.idea.project; this.passwordPolicy = this._env.idea.auth.passwordPolicy; } /** * Prepare the necessary structure to get authorized in Cognito. */ prepareAuthDetails(username, pwd) { return new AuthenticationDetails({ Username: username, Password: pwd }); } /** * Prepare the necessary structure to identify a Cognito user. */ prepareCognitoUser(username) { return new CognitoUser({ Username: username, Pool: this.userPool }); } /** * Prepare a user attribute (they are all strings) in Cognito's format. */ prepareUserAttribute(name, value) { return new CognitoUserAttribute({ Name: name, Value: value }); } /** * Perform a login through username and password. */ login(username, password) { return new Promise((resolve, reject) => { const user = this.prepareCognitoUser(username); user.authenticateUser(this.prepareAuthDetails(username, password), { onSuccess: () => resolve(this._env.idea.auth.forceLoginWithMFA ? LoginOutcomeActions.MFA_SETUP : LoginOutcomeActions.NONE), onFailure: (err) => reject(err), newPasswordRequired: () => { this.challengeUsername = username; this.challengePassword = password; resolve(LoginOutcomeActions.NEW_PASSWORD); }, totpRequired: () => { this.challengeUsername = username; this.challengePassword = password; resolve(LoginOutcomeActions.MFA_CHALLENGE); } }); }); } /** * Complete the new password flow in the authentication. */ confirmNewPassword(newPassword) { return new Promise((resolve, reject) => { const user = this.prepareCognitoUser(this.challengeUsername); user.authenticateUser(this.prepareAuthDetails(this.challengeUsername, this.challengePassword), { onSuccess: () => resolve(), onFailure: (err) => reject(err), newPasswordRequired: () => user.completeNewPasswordChallenge(newPassword, {}, { onSuccess: () => { this.challengeUsername = null; this.challengePassword = null; resolve(); }, onFailure: (err) => reject(err) }) }); }); } /** * Complete the MFA challenge flow in the authentication. */ completeMFAChallenge(otpCode) { return new Promise((resolve, reject) => { const user = this.prepareCognitoUser(this.challengeUsername); user.authenticateUser(this.prepareAuthDetails(this.challengeUsername, this.challengePassword), { onSuccess: () => resolve(), onFailure: (err) => reject(err), totpRequired: (challengeName) => user.sendMFACode(otpCode, { onSuccess: () => { this.challengeUsername = null; this.challengePassword = null; resolve(); }, onFailure: err => reject(err) }, challengeName) }); }); } /** * Register a new user a set its default attributes. */ register(username, password, attributes) { attributes = attributes || {}; return new Promise((resolve, reject) => { // add attributes like the email address and the fullname const attrs = []; for (const prop in attributes) if (attributes[prop]) attrs.push(this.prepareUserAttribute(prop, attributes[prop])); // add the email, which is equal to the username for most of our Pools if (!attributes.email) attrs.push(this.prepareUserAttribute('email', username)); // register the new user to the pool this.userPool.signUp(username, password, attrs, null, (err, res) => { if (err) return reject(err); this.newAccountJustRegistered = username; resolve(res.user); }); }); } /** * In case a new account has just been registered, return the username. */ getNewAccountJustRegistered() { return this.newAccountJustRegistered; } /** * Confirm a new registration through the confirmation code sent by Cognito. */ confirmRegistration(username, code) { return new Promise((resolve, reject) => { this.prepareCognitoUser(username).confirmRegistration(code, true, (err) => err ? reject(err) : resolve()); }); } /** * Send again a confirmation code for a new registration. */ resendConfirmationCode(username) { return new Promise((resolve, reject) => { this.prepareCognitoUser(username).resendConfirmationCode((err) => (err ? reject(err) : resolve())); }); } /** * Logout the currently signed-in user. */ logout(options) { options = Object.assign({ global: false }, options); return new Promise((resolve, reject) => { // remove the refresh token previosly saved this._storage.remove('AuthRefreshToken').then(() => // remove the optional auth details this._storage.remove('AuthUserDetails').then(() => { // get the user and the session const user = this.userPool.getCurrentUser(); user.getSession(async (err) => { // if the session is active, run the online sign-out; otherwise, only the local data has been deleted if (err) return resolve(); // if a reference to the device was saved, forget it if (this._env.idea.auth.singleSimultaneousSession) await this.setCurrentDeviceForProject(null); // sign-out from the pool (terminate the current session or all the sessions) if (options.global) user.globalSignOut({ onSuccess: () => resolve(), onFailure: err => reject(err) }); else { user.signOut(); resolve(); } }); })); }); } /** * Send a password reset request. */ forgotPassword(username) { return new Promise((resolve, reject) => { this.prepareCognitoUser(username).forgotPassword({ onSuccess: () => resolve(), onFailure: (err) => reject(err) }); }); } /** * Confirm a new password after a password reset request. */ confirmPassword(username, code, newPwd) { return new Promise((resolve, reject) => { this.prepareCognitoUser(username).confirmPassword(code, newPwd, { onSuccess: () => resolve(), onFailure: (err) => reject(err) }); }); } /** * Gets the URL for enabling MFA. This URL can be used to generate a QR Code to read with an authenticator app. */ getURLForEnablingMFA() { return new Promise((resolve, reject) => { const user = this.userPool.getCurrentUser(); if (!user) return reject(); user.getSession((err) => { if (err) return reject(err); user.associateSoftwareToken({ associateSecretCode: (secretCode) => resolve(`otpauth://totp/${encodeURI(this.mfaProjectName)}?secret=${secretCode}`), onFailure: (err) => reject(err) }); }); }); } /** * Check whether the user has MFA enabled. */ checkIfUserHasMFAEnabled(bypassCache = false) { return new Promise((resolve, reject) => { const user = this.userPool.getCurrentUser(); if (!user) return reject(); user.getSession((err) => { if (err) return reject(err); user.getUserData((err, data) => { if (err) return reject(err); const isMFAEnabled = !!(data.UserMFASettingList && data.UserMFASettingList.includes('SOFTWARE_TOKEN_MFA')); resolve(isMFAEnabled); }, { bypassCache }); }); }); } /** * Configure a MFA device for the user. */ setMFADevice(otp, mfaDeviceName, enabled = true, preferred = true) { return new Promise((resolve, reject) => { const user = this.userPool.getCurrentUser(); if (!user) return reject(); user.getSession((err) => { if (err) return reject(err); user.verifySoftwareToken(otp, mfaDeviceName, { onSuccess: () => user.setUserMfaPreference(null, { Enabled: enabled, PreferredMfa: preferred }, (err) => err ? reject(err) : resolve()), onFailure: (err) => reject(err) }); }); }); } /** * Enable a new MFA device for the user by inserting an OTP code generated by it. */ enableMFA(otp, mfaDeviceName = 'default') { return this.setMFADevice(otp, mfaDeviceName, true, true); } /** * Disable MFA for a user by inserting an OTP code generated by it. */ disableMFA(otp, mfaDeviceName = 'default') { return this.setMFADevice(otp, mfaDeviceName, false, false); } /** * Check if a user is currently authenticated. * @param offlineAllowed if set and if offline, skip authentication and retrieve data locally * @param getFreshIdTokenOnExp cb function to execute when the idToken is refreshed */ isAuthenticated(offlineAllowed, getFreshIdTokenOnExp) { return new Promise((resolve, reject) => { if (offlineAllowed && !navigator.onLine) { this._storage.get('AuthUserDetails').then(userDetails => resolve({ idToken: null, userDetails })); // re-execute the method when back online, so that you can retrieve a token to make requests window.addEventListener('online', () => this.isAuthenticated(true, getFreshIdTokenOnExp) // set the new token as if it was refreshed .then(result => getFreshIdTokenOnExp(result.idToken))); } else { const user = this.userPool.getCurrentUser(); if (!user) return reject(); user.getSession((err, session) => { if (err) return reject(err); user.getUserData((err, data) => { if (err) return reject(err); const isMFAEnabled = !!(data.UserMFASettingList && data.UserMFASettingList.includes('SOFTWARE_TOKEN_MFA')); if (!isMFAEnabled && this._env.idea.auth.forceLoginWithMFA) return reject(new Error(LoginOutcomeActions.MFA_SETUP)); user.getUserAttributes((e, attributes) => { if (e) return reject(e); // remap user attributes const userDetails = {}; attributes.forEach((a) => (userDetails[a.getName()] = a.getValue())); // add the user's groups to the attributes const sessionInfo = session.getAccessToken().decodePayload(); const groups = sessionInfo['cognito:groups'] || []; userDetails['groups'] = groups; // run some checks considering the user's groups and devices (based on the project's configuration) this.runPostAuthChecks(userDetails, err => { // in case some check failed, reject the authorisation flow if (err) return reject(err); // (async) save the refresh token so it can be accessed by other procedures this._storage.set('AuthRefreshToken', session.getRefreshToken().getToken()); // set a timer to manage the autorefresh of the idToken (through the refreshToken) setTimeout(() => this.refreshSession(user, session.getRefreshToken().getToken(), getFreshIdTokenOnExp), 15 * 60 * 1000); // every 15 minutes // (async) if offlineAllowed, save data locally, to use it next time we'll be offline if (offlineAllowed) this._storage.set('AuthUserDetails', userDetails); // return the idToken (to use with API) resolve({ idToken: session.getIdToken().getJwtToken(), userDetails }); }); }); }, { bypassCache: true }); }); } }); } /** * Helper to refresh the session every N minutes. */ refreshSession(user, refreshToken, callback) { user.refreshSession(new CognitoRefreshToken({ RefreshToken: refreshToken }), (err, session) => { if (err) { // try again in 1 minute setTimeout(() => this.refreshSession(user, refreshToken, callback), 1 * 60 * 1000); } else { // (async) save the refresh token so it can be accessed by other procedures this._storage.set('AuthRefreshToken', session.getRefreshToken().getToken()); // repeat every 15 minutes setTimeout(() => this.refreshSession(user, session.getRefreshToken().getToken(), callback), 15 * 60 * 1000); // run the callback action, if set if (callback) callback(session.getIdToken().getJwtToken()); } }); } /** * Run some post-auth checks, based on the users groups and on the app's configuration: * - users in the Cognito's "admins" grup skip all the following rules. * - users in the Cognito's "robots" group can't sign-into front-ends (they serve only back-end purposes). * - if `env.idea.auth.singleSimultaneousSession` is on, make sure there is only one active session per user. */ runPostAuthChecks(userDetails, callback) { const groups = userDetails['groups']; // skip checks if the user is in the "admins" group const isAdmin = groups.some(x => x === 'admins'); if (isAdmin) return callback(); // users in the "robots" group can't sign-into front-ends (they serve only back-end purposes) const isRobot = groups.some(x => x === 'robots'); if (isRobot) return callback(new Error('ROBOT_USER')); // in case the project limits each account to only one simultaneous session, run a check if (this._env.idea.auth.singleSimultaneousSession) return this.checkForSimultaneousSessions(userDetails, callback); // otherwise, we're done callback(); } /** * Check whether the user signed-into multiple devices. */ async checkForSimultaneousSessions(userDetails, callback) { // get or create a key for the current device (~random) let currentDeviceKey = await this._storage.get('AuthDeviceKey'); if (!currentDeviceKey) { const randomKey = Math.random().toString(36).substring(10); currentDeviceKey = randomKey.concat(String(Date.now())); await this._storage.set('AuthDeviceKey', currentDeviceKey); } // check whether the last device to sign-into this project (if any) is the current one const lastDeviceToSignIntoThisProject = userDetails[this.deviceKeyAttribute]; // if it's the first user's device to sign-in, save the reference in Cognito and resolve if (!lastDeviceToSignIntoThisProject) { this.setCurrentDeviceForProject(currentDeviceKey) .then(() => callback()) .catch(() => callback(new Error('CANT_SET_DEVICE'))); // if the device didn't change, resolve } else if (lastDeviceToSignIntoThisProject === currentDeviceKey) callback(); // othwerwise, report there is more than one simultaneous session else callback(new Error('SIMULTANEOUS_SESSION')); } /** * Set (or reset) the current user's device (by key) in the current project (stored in Cognito). */ setCurrentDeviceForProject(deviceKey) { const attributes = {}; // note: an empty string will remove the attribute from Cognito attributes[this.deviceKeyAttribute] = deviceKey || ''; return this.updateUserAttributes(attributes); } /** * Update the currently logged in user's attributes. */ updateUserAttributes(attributes) { return new Promise((resolve, reject) => { // prepare the attributes we want to change const attrs = new Array(); for (const prop in attributes) if (attributes[prop] !== undefined) attrs.push(this.prepareUserAttribute(prop, attributes[prop])); const user = this.userPool.getCurrentUser(); if (!user) return reject(); // we need to get the session before to make changes user.getSession((err) => { if (err) reject(err); else user.updateAttributes(attrs, (e) => (e ? reject(e) : resolve())); }); }); } /** * Validate the password against the policy set in the environments configuration. * In case there are errors, they are returned as an array of strings. */ validatePasswordAgainstPolicy(password) { const errors = []; if (password?.trim().length < this.passwordPolicy.minLength) errors.push('MIN_LENGTH'); if (this.passwordPolicy.requireDigits && !/\d/.test(password)) errors.push('REQUIRE_DIGITS'); if (this.passwordPolicy.requireLowercase && !/[a-z]/.test(password)) errors.push('REQUIRE_LOWERCASE'); if (this.passwordPolicy.requireSymbols && !/[\^\$\*\.\_\~\`\+\=@\!\?\>\<\:\;\\\,\#\%\&]/.test(password)) errors.push('REQUIRE_SYMBOLS'); if (this.passwordPolicy.requireUppercase && !/[A-Z]/.test(password)) errors.push('REQUIRE_UPPERCASE'); return errors; } /** * Validate the password against offline and online databases, to avoid common and exploited passwords. */ async validatePasswordAgainstDatabases(password, customStringsToAvoid) { const errors = []; let pwd = (password || '').trim().toLowerCase(); const pwdContains = (str) => pwd.includes((str || '').trim().toLowerCase()); const projectReferencesToAvoid = [ this._env.idea.project, this._env.idea.auth.title, this._translate._('COMMON.APP_NAME') ]; if (projectReferencesToAvoid.some(x => pwdContains(x))) errors.push('COMMON_PASSWORD_PROJECT'); if (customStringsToAvoid && customStringsToAvoid.some(x => pwdContains(x))) errors.push('COMMON_PASSWORD_CUSTOM'); if (COMMON_PASSWORDS_TOP_1000.some(x => pwdContains(x))) errors.push('COMMON_PASSWORD_INTERNAL'); if (errors.length === 0) { const pwdSHA1 = await SHA1(password); if (pwdSHA1) { const res = await fetch('https://api.pwnedpasswords.com/range/'.concat(pwdSHA1.slice(0, 5))); if (res.status === 200) { const pwnedPasswordsRawSuffixes = await res.text(); if (pwnedPasswordsRawSuffixes.includes(pwdSHA1.slice(5))) errors.push('COMMON_PASSWORD_EXTERNAL'); } } } return errors; } /** * Get a complete password policy pattern, based on the environments configuration, to use on password input fields. * Note: some of the symbols couldn't be included because unsupported by the input[pattern] attribute. */ getPasswordPolicyPatternForInput() { let pattern = ''; if (this.passwordPolicy.requireDigits) pattern += `(?=.*[0-9])`; if (this.passwordPolicy.requireLowercase) pattern += `(?=.*[a-z])`; if (this.passwordPolicy.requireSymbols) pattern += `(?=.*[\^\$\*\.\_\~\`\+\=@\!\?\>\<\:\;\\\,\#\%\&])`; if (this.passwordPolicy.requireUppercase) pattern += `(?=.*[A-Z])`; pattern += `.{${this.passwordPolicy.minLength},}`; return pattern; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAuthService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAuthService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * The possible actions following a successful login. */ var LoginOutcomeActions; (function (LoginOutcomeActions) { LoginOutcomeActions["NONE"] = "none"; LoginOutcomeActions["NEW_PASSWORD"] = "newPassword"; LoginOutcomeActions["MFA_CHALLENGE"] = "mfaChallenge"; LoginOutcomeActions["MFA_SETUP"] = "mfaSetup"; })(LoginOutcomeActions || (LoginOutcomeActions = {})); /** * Get the SHA1 hex uppercase hash of a string, using web crypto. */ const SHA1 = async (str) => { if (!window.crypto) return null; const msgUint8 = new TextEncoder().encode(str); const hashBuffer = await window.crypto.subtle.digest('SHA-1', msgUint8); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return hashHex.toUpperCase(); }; class IDEASignInPage { constructor() { this._env = inject(IDEAEnvironment); this._nav = inject(NavController); this._message = inject(IDEAMessageService); this._loading = inject(IDEALoadingService); this._translate = inject(IDEATranslationsService); this._auth = inject(IDEAAuthService); this.doneExternalProviderCheck = false; this.agreementsCheck = true; this.newAccountRegistered = false; this.darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; this.title = this._env.idea.auth.title; this.registrationPossible = this._env.idea.auth.registrationIsPossible; this.hasIntroPage = this._env.idea.auth.hasIntroPage; this.website = this._env.idea.auth.website; this.externalProviders = this._env.aws.cognito?.externalProviders || []; } ionViewDidEnter() { // manage the scenario in which we just created a new account (show a explanatory message: email must be confirmed) this.newAccountRegistered = !!this._auth.getNewAccountJustRegistered(); if (this.newAccountRegistered) this.email = this._auth.getNewAccountJustRegistered(); } async login() { if (!this.agreementsCheck) return; try { this.errorMsg = null; await this._loading.show(); const loginAction = await this._auth.login(this.email, this.password); if (loginAction === LoginOutcomeActions.NEW_PASSWORD) this._nav.navigateForward(['auth', 'new-password']); else if (loginAction === LoginOutcomeActions.MFA_CHALLENGE) this._nav.navigateForward(['auth', 'mfa-challenge']); else if (loginAction === LoginOutcomeActions.MFA_SETUP) this._nav.navigateForward(['auth', 'setup-mfa']); else window.location.assign(''); } catch (err) { if (err.name === 'UserNotConfirmedException') this.errorMsg = this._translate._('IDEA_AUTH.CONFIRM_YOUR_EMAIL_TO_LOGIN'); else if (err.name === 'UserLambdaValidationException' && err.message?.includes('@IDEA_COGNITO_TRANSITION')) this.errorMsg = this._translate._('IDEA_AUTH.CHANGE_YOUR_PASSWORD_TO_LOGIN'); this._message.error('IDEA_AUTH.AUTHENTICATION_FAILED'); } finally { this._loading.hide(); } } checkForExternalProviderEmail() { if (isEmpty(this.email, 'email')) return; const emailDomain = this.email.split('@')[1]; this.doneExternalProviderCheck = true; const provider = this.externalProviders.find(p => p.emailDomains.includes(emailDomain)); if (provider) this._nav.navigateForward(['auth', provider.type], { queryParams: { provider: provider.name, go: true } }); } goToIntro() { this._nav.navigateBack(['intro']); } goToForgotPassword() { this._nav.navigateForward(['auth', 'forgot-password']); } goToRegistration() { this._nav.navigateForward(['auth', 'sign-up']); } translationExists(key) { return !!this._translate._(key); } async openLink(url) { await Browser.open({ url }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEASignInPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEASignInPage, isStandalone: true, selector: "idea-sign-in", ngImport: i0, template: ` <ion-content> @if (newAccountRegistered) { <ion-card color="warning"> <ion-card-content>{{ 'IDEA_AUTH.CONFIRM_YOUR_EMAIL_TO_LOGIN' | translate }}</ion-card-content> </ion-card> } <form class="flexBox"> <div> <ion-card class="authCard"> <ion-img [title]="'IDEA_AUTH.SERVICE_LOGO' | translate" [src]="darkMode ? 'assets/icons/icon-auth-alt.svg' : 'assets/icons/icon-auth.svg'" /> <ion-card-header> @if (title) { <ion-card-title color="primary">{{ title }}</ion-card-title> } @if (translationExists('IDEA_VARIABLES.TAGLINE')) { <ion-card-subtitle> {{ 'IDEA_VARIABLES.TAGLINE' | translate }} </ion-card-subtitle> } </ion-card-header> <ion-card-content> @if (errorMsg) { <p testId="signin.error" class="errorBox"> <b>{{ 'IDEA_AUTH.WARNING' | translate }}.</b> {{ errorMsg }} </p> } <ion-item> <ion-label position="inline"> <ion-icon name="person-circle" color="primary" /> </ion-label> <ion-input testId="signin.email" type="email" inputmode="email" pattern="[A-Za-z0-9._%+-]{2,}@[a-zA-Z-_.]{2,}[.]{1}[a-zA-Z]{2,}" spellcheck="false" autocorrect="off" autocomplete="email" [placeholder]="'IDEA_AUTH.EMAIL' | translate" [title]="'IDEA_AUTH.EMAIL_HINT' | translate" [disabled]="externalProviders.length && doneExternalProviderCheck" [ngModelOptions]="{ standalone: true }" [(ngModel)]="email" (keyup.enter)="externalProviders.length ? checkForExternalProviderEmail() : login()" /> @if (externalProviders.length && doneExternalProviderCheck) { <ion-button slot="end" fill="clear" [title]="'IDEA_AUTH.EDIT_EMAIL' | translate" (click)="doneExternalProviderCheck = false" > <ion-icon icon="pencil" slot="icon-only" /> </ion-button> } </ion-item> @if (externalProviders.length && !doneExternalProviderCheck) { <ion-button testId="continueButton" expand="block" [disabled]="!agreementsCheck" [title]="'IDEA_AUTH.CONTINUE_HINT' | translate" (click)="checkForExternalProviderEmail()" > {{ 'IDEA_AUTH.CONTINUE' | translate }} </ion-button> } @else { <ion-item> <ion-label position="inline"> <ion-icon name="key" color="primary" /> </ion-label> <ion-input testId="signin.password" id="current-password" type="password" spellcheck="false" autocorrect="off" autocomplete="current-password" [clearOnEdit]="false" [placeholder]="'IDEA_AUTH.PASSWORD' | translate" [title]="'IDEA_AUTH.PASSWORD_HINT' | translate" [ngModelOptions]="{ standalone: true }" [(ngModel)]="password" (keyup.enter)="login()" /> </ion-item> @if ( !registrationPossible && (translationExists('IDEA_VARIABLES.TERMS_AND_CONDITIONS_URL') || translationExists('IDEA_VARIABLES.PRIVACY_POLICY_URL')) ) { <ion-row class="agreementsAcceptanceRow"> <ion-col> <ion-checkbox size="small" [ngModelOptions]="{ standalone: true }" [(ngModel)]="agreementsCheck" /> {{ 'IDEA_AUTH.AGREEMENTS_CHECK_LOGIN' | translate }}: @if (translationExists('IDEA_VARIABLES.TERMS_AND_CONDITIONS_URL')) { <a ion-text testId="openTermsAndConditionsButton" color="primary" target="_blank" [title]="'IDEA_AUTH.TERMS_AND_CONDITIONS_HINT' | translate" [href]="'IDEA_VARIABLES.TERMS_AND_CONDITIONS_URL' | translate" > {{ 'IDEA_AUTH.TERMS_AND_CONDITIONS' | translate }} </a> } @if ( translationExists('IDEA_VARIABLES.PRIVACY_POLICY_URL') && translationExists('IDEA_VARIABLES.TERMS_AND_CONDITIONS_URL') ) { <ion-text> & </ion-text> } @if (translationExists('IDEA_VARIABLES.PRIVACY_POLICY_URL')) { <a ion-text testId="openPrivacyPolicyButton" color="primary" target="_blank" [title]="'IDEA_AUTH.PRIVACY_POLICY_HINT' | translate" [href]="'IDEA_VARIABLES.PRIVACY_POLICY_URL' | translate" > {{ 'IDEA_AUTH.PRIVACY_POLICY' | translate }} </a> } </ion-col> </ion-row> } <ion-button testId="signInButton" expand="block" [disabled]="!agreementsCheck" [title]="'IDEA_AUTH.SIGN_IN_HINT' | translate" (click)="login()" > {{ 'IDEA_AUTH.SIGN_IN' | translate }} </ion-button> @if (registrationPossible) { <ion-button testId="createAnAccountButton" fill="clear" expand="block" class="smallCaseButton" [title]="'IDEA_AUTH.CREATE_AN_ACCOUNT_HINT' | translate" (click)="goToRegistration()" > {{ 'IDEA_AUTH.CREATE_AN_ACCOUNT' | translate }} </ion-button> } <ion-button testId="forgotPasswordButton" fill="clear" expand="block" class="smallCaseButton" [title]="'IDEA_AUTH.I_FORGOT_MY_PASSWORD_HINT' |