From b1ccefbffb663ac0a838cd89e3e360c249b0031d Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Tue, 2 Apr 2024 13:19:57 -0700 Subject: [PATCH 01/11] Reuse tokens. Rename API class. Prefer single quotes Node --- functions/auth0/.eslintrc.js | 26 ++- functions/auth0/api/auth0.js | 195 +++++++++--------- functions/auth0/index.js | 20 +- functions/auth0/package-lock.json | 76 ++----- functions/auth0/package.json | 3 +- functions/auth0/utils/auth0.js | 81 ++++---- .../{api.dart => auth0_user_api.dart} | 0 ...art => auth0_user_api_implementation.dart} | 33 ++- lib/views/dialogs/authenticator.dart | 14 +- lib/views/dialogs/mobile.dart | 6 +- .../screens/advanced_security_screen.dart | 20 +- lib/views/screens/home_screen.dart | 7 +- lib/views/screens/password_screen.dart | 12 +- lib/views/screens/profile_screen.dart | 6 +- test/advanced_test.dart | 8 +- test/auth0_api_test.dart | 2 +- ...st.mocks.dart => auth0_user_api_mock.dart} | 4 +- test/password_test.dart | 4 +- 18 files changed, 253 insertions(+), 264 deletions(-) rename lib/controllers/{api.dart => auth0_user_api.dart} (100%) rename lib/controllers/{api_implementation.dart => auth0_user_api_implementation.dart} (90%) rename test/mocks/{advanced_test.mocks.dart => auth0_user_api_mock.dart} (97%) diff --git a/functions/auth0/.eslintrc.js b/functions/auth0/.eslintrc.js index e14a23c..2c411ac 100644 --- a/functions/auth0/.eslintrc.js +++ b/functions/auth0/.eslintrc.js @@ -4,24 +4,28 @@ module.exports = { node: true, }, parserOptions: { - "ecmaVersion": 2020, + 'ecmaVersion': 2020, }, extends: [ - "eslint:recommended", - "google", + 'eslint:recommended', + 'google', ], rules: { - "no-restricted-globals": ["error", "name", "length"], - "prefer-arrow-callback": "error", - "quotes": ["error", "double", {"allowTemplateLiterals": true}], - "comma-dangle": 0, - "require-jsdoc": 0, - "one-var": 0, - "linebreak-style": 0 + 'no-restricted-globals': ['error', 'name', 'length'], + 'prefer-arrow-callback': 'error', + 'comma-dangle': 0, + 'require-jsdoc': 0, + 'one-var': 0, + 'linebreak-style': 0, + 'quotes': [2, 'single', { + 'avoidEscape': true, + 'allowTemplateLiterals': true + }], + 'indent': [1], }, overrides: [ { - files: ["**/*.spec.*"], + files: ['**/*.spec.*'], env: { mocha: true, }, diff --git a/functions/auth0/api/auth0.js b/functions/auth0/api/auth0.js index 0835a33..d56c641 100644 --- a/functions/auth0/api/auth0.js +++ b/functions/auth0/api/auth0.js @@ -1,14 +1,14 @@ -const {onRequest} = require("firebase-functions/v2/https"); -const axios = require("axios"); -const {User} = require("../models/user"); +const {onRequest} = require('firebase-functions/v2/https'); +const axios = require('axios'); +const {User} = require('../models/user'); const { auth0Domain, auth0ClientId, auth0ClientSecret, -} = require("../utils/constants"); +} = require('../utils/constants'); -const {getAccessToken, authorizeUser} = require("../utils/auth0"); +const {getAccessToken, authorizeUser} = require('../utils/auth0'); const updateUser = onRequest(async (req, res) => { let user; @@ -23,38 +23,38 @@ const updateUser = onRequest(async (req, res) => { const updatedUserObject = {}; if (user.firstName) { - updatedUserObject["given_name"] = user.firstName; + updatedUserObject['given_name'] = user.firstName; } if (user.lastName) { - updatedUserObject["family_name"] = user.lastName; + updatedUserObject['family_name'] = user.lastName; } const primaryAddress = {}; if (user.zip) { - primaryAddress["zip"] = user.zip; + primaryAddress['zip'] = user.zip; } if (user.address) { - primaryAddress["address"] = user.address; + primaryAddress['address'] = user.address; } if (user.address2) { - primaryAddress["address2"] = user.address2; + primaryAddress['address2'] = user.address2; } if (user.state) { - primaryAddress["state"] = user.state; + primaryAddress['state'] = user.state; } if (user.city) { - primaryAddress["city"] = user.city; + primaryAddress['city'] = user.city; } - const metaAddresses = user.metadata["addresses"]; + const metaAddresses = user.metadata['addresses']; if (metaAddresses) { - metaAddresses["primary"] = primaryAddress; + metaAddresses['primary'] = primaryAddress; } else { user.metadata = { addresses: { @@ -63,61 +63,66 @@ const updateUser = onRequest(async (req, res) => { }; } - user.metadata["phone"] = user.phone; - updatedUserObject["user_metadata"] = user.metadata; + user.metadata['phone'] = user.phone; + updatedUserObject['user_metadata'] = user.metadata; const updateUserUrl = `https://${auth0Domain}/api/v2/users/${user.userId}`; const token = await getAccessToken(); const headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}`, }; try { - const request = await axios.patch(updateUserUrl, updatedUserObject, { + await axios.patch(updateUserUrl, updatedUserObject, { headers, }); - if (request.status === 200) { - return res.status(200).send(request.data); - } else { - return res.sendStatus(request.status); - } + return res.status(200).send(); } catch (err) { - console.error(err); - return res.send(err); + console.error(JSON.stringify(err)); + return res.status(err?.response?.status || 500).send(err.message); } }); const updatePassword = onRequest(async (req, res) => { const body = req.body; + const { + email, + oldPassword, + newPassword, + userId + } = body; + + if (!email.length || !oldPassword.length || + !newPassword.length || !userId.length) { + res.status(400).send('Invalid request'); + return; + } + try { - const validateResponse = await authorizeUser(body.email, body.oldPassword); - - if (validateResponse.status === 200) { - const auth0Token = await getAccessToken(); - const passwordUpdateRequest = { - method: "PATCH", - url: `https://${auth0Domain}/api/v2/users/${body.userId}`, - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${auth0Token}`, - }, - data: { - password: body.newPassword, - connection: "Username-Password-Authentication", - }, - }; + await authorizeUser(email, oldPassword); - const updateResponse = await axios.request(passwordUpdateRequest); + const auth0Token = await getAccessToken(); + const passwordUpdateRequest = { + method: 'PATCH', + url: `https://${auth0Domain}/api/v2/users/${userId}`, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${auth0Token}`, + }, + data: { + password: newPassword, + connection: 'Username-Password-Authentication', + }, + }; - if (updateResponse.status === 200) { - res.status(200).send(); - return; - } - } + await axios.request(passwordUpdateRequest); + + res.status(200).send(); + return; } catch (err) { console.error(`Error: ${err.message}`); @@ -128,7 +133,7 @@ const updatePassword = onRequest(async (req, res) => { res .status(status) - .send(message || error_description || "Error encountered"); + .send(message || error_description || 'Error encountered'); return; } }); @@ -140,11 +145,11 @@ const authMethods = onRequest(async (req, res) => { const auth0Token = await getAccessToken(); const config = { - method: "get", + method: 'get', maxBodyLength: Infinity, url: `https://${auth0Domain}/api/v2/users/${body.userId}/authentication-methods`, headers: { - Accept: "application/json", + Accept: 'application/json', Authorization: `Bearer ${auth0Token}`, }, }; @@ -171,41 +176,36 @@ const enrollMFA = onRequest(async (req, res) => { const validateResponse = await authorizeUser( email, password, - "/mfa/" + '/mfa/' ); - if (validateResponse.status === 200) { - const mfaToken = validateResponse?.data?.access_token; - - let additionalData = {}; - - if (mfaFactor == "oob") { - additionalData = { - "oob_channels": [channel], - "phone_number": number - } - } - - const otpRequest = { - method: "POST", - url: `https://${auth0Domain}/mfa/associate`, - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${mfaToken}`, - }, - data: { - authenticator_types: [mfaFactor], - ...additionalData - }, - }; + const mfaToken = validateResponse?.data?.access_token; - const otpResponse = await axios.request(otpRequest); - otpResponse.data.token = mfaToken; + let additionalData = {}; - if (otpResponse.status === 200) { - res.status(200).send(otpResponse?.data); - } + if (mfaFactor == 'oob') { + additionalData = { + 'oob_channels': [channel], + 'phone_number': number + }; } + + const otpRequest = { + method: 'POST', + url: `https://${auth0Domain}/mfa/associate`, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${mfaToken}`, + }, + data: { + authenticator_types: [mfaFactor], + ...additionalData + }, + }; + + const otpResponse = await axios.request(otpRequest); + otpResponse.data.token = mfaToken; + res.status(200).send(otpResponse?.data); } catch (err) { console.error(err); @@ -213,10 +213,10 @@ const enrollMFA = onRequest(async (req, res) => { // Status Code for failed Authorization if (code === 403) { - message = "Invalid Password."; + message = 'Invalid Password.'; } - res.status(code || 500).send({error: message || "Error encountered"}); + res.status(code || 500).send({error: message || 'Error encountered'}); } }); @@ -224,30 +224,29 @@ const confirmMFA = onRequest(async (req, res) => { const body = req.body; const { - mfaToken, + mfaToken, userOtpCode = '', oobCode = '', } = body; try { - let additionalData = {}; - if (oobCode.length) { + if (oobCode.length) { additionalData = { oob_code: `${oobCode}`, binding_code: `${userOtpCode}` - } + }; } else { additionalData = { otp: `${userOtpCode}` - } + }; } const options = { - method: "POST", + method: 'POST', url: `https://${auth0Domain}/oauth/token`, - headers: {"content-type": "application/x-www-form-urlencoded"}, + headers: {'content-type': 'application/x-www-form-urlencoded'}, data: new URLSearchParams({ grant_type: `http://auth0.com/oauth/grant-type/${oobCode.length ? 'mfa-oob' : 'mfa-otp'}`, client_id: `${auth0ClientId}`, @@ -257,13 +256,11 @@ const confirmMFA = onRequest(async (req, res) => { }), }; - const enrollment = await axios.request(options); + await axios.request(options); - if (enrollment.status === 200) { - res.sendStatus(200); - } + res.sendStatus(200); } catch (err) { - let customError = ""; + let customError = ''; const { status, @@ -271,7 +268,7 @@ const confirmMFA = onRequest(async (req, res) => { } = err?.response; if (status === 403) { - customError = "Invalid code."; + customError = 'Invalid code.'; } res.status(status).send({error: customError || error_description}); @@ -285,11 +282,11 @@ const unenrollMFA = onRequest(async (req, res) => { const auth0Token = await getAccessToken(); const config = { - method: "delete", + method: 'delete', maxBodyLength: Infinity, url: `https://${auth0Domain}/api/v2/users/${body.userId}/authentication-methods/${body.authFactorId}`, headers: { - "Authorization": `Bearer ${auth0Token}`, + 'Authorization': `Bearer ${auth0Token}`, }, }; diff --git a/functions/auth0/index.js b/functions/auth0/index.js index 9cfa2f6..627c3f4 100644 --- a/functions/auth0/index.js +++ b/functions/auth0/index.js @@ -1,6 +1,6 @@ -const {onRequest} = require("firebase-functions/v2/https"); -const admin = require("firebase-admin"); -const express = require("express"); +const {onRequest} = require('firebase-functions/v2/https'); +const admin = require('firebase-admin'); +const express = require('express'); admin.initializeApp(); const app = express(); @@ -12,15 +12,15 @@ const { confirmMFA, authMethods, unenrollMFA -} = require("./api/auth0"); +} = require('./api/auth0'); app.use(express.json()); -app.post("/auth0/updateUser", updateUser); -app.post("/auth0/updatePassword", updatePassword); -app.post("/auth0/enrollMFA", enrollMFA); -app.post("/auth0/confirmMFA", confirmMFA); -app.post("/auth0/authMethods", authMethods); -app.post("/auth0/unenrollMFA", unenrollMFA); +app.post('/auth0/updateUser', updateUser); +app.post('/auth0/updatePassword', updatePassword); +app.post('/auth0/enrollMFA', enrollMFA); +app.post('/auth0/confirmMFA', confirmMFA); +app.post('/auth0/authMethods', authMethods); +app.post('/auth0/unenrollMFA', unenrollMFA); exports.auth0 = onRequest(app); diff --git a/functions/auth0/package-lock.json b/functions/auth0/package-lock.json index 816841a..0986927 100644 --- a/functions/auth0/package-lock.json +++ b/functions/auth0/package-lock.json @@ -11,7 +11,8 @@ "axios": "^1.5.1", "express": "^4.18.2", "firebase-admin": "^11.8.0", - "firebase-functions": "^4.3.1" + "firebase-functions": "^4.3.1", + "jsonwebtoken": "9.0.2" }, "devDependencies": { "eslint": "^8.15.0", @@ -2331,11 +2332,11 @@ } }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -2843,9 +2844,9 @@ "peer": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -3473,16 +3474,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -3513,43 +3514,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3794,9 +3758,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -5287,9 +5251,9 @@ } }, "node_modules/jose": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.2.tgz", - "integrity": "sha512-IY73F228OXRl9ar3jJagh7Vnuhj/GzBunPiZP13K0lOl7Am9SoWW3kEzq3MCllJMTtZqHTiDXQvoRd4U95aU6A==", + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", "funding": { "url": "https://github.com/sponsors/panva" } diff --git a/functions/auth0/package.json b/functions/auth0/package.json index 9a6e475..116aa93 100644 --- a/functions/auth0/package.json +++ b/functions/auth0/package.json @@ -19,7 +19,8 @@ "axios": "^1.5.1", "express": "^4.18.2", "firebase-admin": "^11.8.0", - "firebase-functions": "^4.3.1" + "firebase-functions": "^4.3.1", + "jsonwebtoken": "9.0.2" }, "devDependencies": { "eslint": "^8.15.0", diff --git a/functions/auth0/utils/auth0.js b/functions/auth0/utils/auth0.js index 3cd849f..920d351 100644 --- a/functions/auth0/utils/auth0.js +++ b/functions/auth0/utils/auth0.js @@ -1,66 +1,75 @@ -const axios = require("axios"); +const jwt = require('jsonwebtoken'); +const axios = require('axios'); + const { auth0Domain, auth0ClientId, auth0ClientSecret -} = require("./constants.js"); +} = require('./constants.js'); + +let auth0Token; const getAccessToken = async () => { + if (auth0Token) { + const decodedToken = await jwt.decode(auth0Token); + const tokenExpiration = decodedToken.exp * 1000; + const now = Date.now(); + const tokenValid = tokenExpiration > now; + if (tokenValid) { + return auth0Token; + } + } + const options = { - "Content-Type": "application/x-www-form-urlencoded" + 'Content-Type': 'application/x-www-form-urlencoded' }; const body = { - "grant_type": "client_credentials", - "client_id": auth0ClientId, - "client_secret": auth0ClientSecret, - "audience": `https://${auth0Domain}/api/v2/` + 'grant_type': 'client_credentials', + 'client_id': auth0ClientId, + 'client_secret': auth0ClientSecret, + 'audience': `https://${auth0Domain}/api/v2/` }; - const request = await axios.post( - `https://${auth0Domain}/oauth/token`, - body, { - headers: options - } - ); + try { + const request = await axios.post( + `https://${auth0Domain}/oauth/token`, + body, { + headers: options + } + ); - if (request.status === 200) { - return request.data.access_token; + auth0Token = request.data.access_token; + return auth0Token; + } catch (err) { + console.error(err); + throw new Error('Error getting access token'); } }; -const authorizeUser = async (email, password, audience = "/api/v2/") => { +const authorizeUser = async (email, password, audience = '/api/v2/') => { try { const passwordValidationRequest = { - method: "POST", + method: 'POST', url: `https://${auth0Domain}/oauth/token`, headers: { - "Content-Type": "application/x-www-form-urlencoded" + 'Content-Type': 'application/x-www-form-urlencoded' }, data: new URLSearchParams({ - "grant_type": "password", - "username": email, - "password": password, - "client_id": auth0ClientId, - "client_secret": auth0ClientSecret, - "audience": `https://${auth0Domain}${audience}`, - "scope": audience === "/api/v2/" ? "openid" : "" + 'grant_type': 'password', + 'username': email, + 'password': password, + 'client_id': auth0ClientId, + 'client_secret': auth0ClientSecret, + 'audience': `https://${auth0Domain}${audience}`, + 'scope': audience === '/api/v2/' ? 'openid' : '' }) }; return await axios.request(passwordValidationRequest); } catch (err) { - const { - status, - data: { - error_description, - } - } = err?.response; - - const error = new Error(error_description || "Auth0 Authorization Failed"); - error.code = status || 500; - - throw error; + console.error(err); + throw err; } }; diff --git a/lib/controllers/api.dart b/lib/controllers/auth0_user_api.dart similarity index 100% rename from lib/controllers/api.dart rename to lib/controllers/auth0_user_api.dart diff --git a/lib/controllers/api_implementation.dart b/lib/controllers/auth0_user_api_implementation.dart similarity index 90% rename from lib/controllers/api_implementation.dart rename to lib/controllers/auth0_user_api_implementation.dart index 99cd58b..3d8f64f 100644 --- a/lib/controllers/api_implementation.dart +++ b/lib/controllers/auth0_user_api_implementation.dart @@ -7,10 +7,12 @@ import 'package:angeleno_project/models/password_reset.dart'; import 'package:angeleno_project/utils/constants.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:http/http.dart' as http; -import 'package:angeleno_project/controllers/api.dart'; +import 'package:angeleno_project/controllers/auth0_user_api.dart'; import 'package:angeleno_project/models/user.dart'; -class UserApi extends Api { +class Auth0UserApi extends Api { + + var authToken = ''; String createJwt() { final jwt = JWT( @@ -40,30 +42,40 @@ class UserApi extends Api { } Future getOAuthToken() async { - String newToken = ''; - final createdToken = createJwt(); + if (authToken.isNotEmpty) { + final decodedToken = JWT.decode(authToken); + final tokenExpiration = decodedToken.payload['exp'] as int; + if (DateTime + .now() + .millisecondsSinceEpoch ~/ 1000 < tokenExpiration) { + return authToken; + } + } try { + final jwt = createJwt(); + final response = await http.post( Uri.parse('https://www.googleapis.com/oauth2/v4/token'), headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': 'Bearer $createdToken' + 'Authorization': 'Bearer $jwt' }, // ignore: lines_longer_than_80_chars - body: 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=$createdToken' - ).timeout(const Duration(seconds: 15)); + body: 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=$jwt' + ).timeout(const Duration(seconds: 5)); if (response.statusCode == HttpStatus.ok) { final jsonRes = jsonDecode(response.body); - newToken = jsonRes['id_token'] as String; + authToken = jsonRes['id_token'] as String; + return authToken; } } catch (err) { print(err); } - return newToken; + throw Exception('No token received'); } @override @@ -72,6 +84,7 @@ class UserApi extends Api { try { final token = await getOAuthToken(); + print(token); if (token.isEmpty) { throw const FormatException('Empty token received'); @@ -88,7 +101,7 @@ class UserApi extends Api { Uri.parse('/auth0/updateUser'), headers: headers, body: body - ).timeout(const Duration(seconds: 15)); + ).timeout(const Duration(seconds: 5)); if (response.statusCode == HttpStatus.ok) { print(response.body); diff --git a/lib/views/dialogs/authenticator.dart b/lib/views/dialogs/authenticator.dart index e56198f..cc1b74c 100644 --- a/lib/views/dialogs/authenticator.dart +++ b/lib/views/dialogs/authenticator.dart @@ -3,17 +3,17 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import '../../controllers/api_implementation.dart'; +import '../../controllers/auth0_user_api_implementation.dart'; import '../../controllers/user_provider.dart'; import '../../utils/constants.dart'; class AuthenticatorDialog extends StatefulWidget { final UserProvider userProvider; - final UserApi userApi; + final Auth0UserApi auth0UserApi; const AuthenticatorDialog({ required this.userProvider, - required this.userApi, + required this.auth0UserApi, super.key }); @@ -27,7 +27,7 @@ class _AuthenticatorDialogState extends State { final passwordField = TextEditingController(); late UserProvider userProvider; - late UserApi api; + late Auth0UserApi auth0UserApi; int _pageIndex = 0; String errMsg = ''; @@ -44,7 +44,7 @@ class _AuthenticatorDialogState extends State { super.initState(); userProvider = widget.userProvider; - api = widget.userApi; + auth0UserApi = widget.auth0UserApi; } @override @@ -91,7 +91,7 @@ class _AuthenticatorDialogState extends State { 'mfaFactor': 'otp' }; - api.enrollMFA(body).then((final response) { + auth0UserApi.enrollMFA(body).then((final response) { final bool success = response['status'] == HttpStatus.ok; if (success) { setState(() { @@ -123,7 +123,7 @@ class _AuthenticatorDialogState extends State { 'userOtpCode': totpCode }; - api.confirmMFA(body).then((final response) { + auth0UserApi.confirmMFA(body).then((final response) { if (response.statusCode == HttpStatus.ok) { Navigator.pop(context, response.statusCode.toString()); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( diff --git a/lib/views/dialogs/mobile.dart b/lib/views/dialogs/mobile.dart index 548c04e..b76c15e 100644 --- a/lib/views/dialogs/mobile.dart +++ b/lib/views/dialogs/mobile.dart @@ -1,5 +1,5 @@ import 'dart:io'; -import 'package:angeleno_project/controllers/api_implementation.dart'; +import 'package:angeleno_project/controllers/auth0_user_api_implementation.dart'; import 'package:flutter/material.dart'; import 'package:intl_phone_number_input/intl_phone_number_input.dart'; @@ -8,7 +8,7 @@ import '../../utils/constants.dart'; class MobileDialog extends StatefulWidget { final UserProvider userProvider; - final UserApi userApi; + final Auth0UserApi userApi; final String channel; const MobileDialog({ @@ -29,7 +29,7 @@ class _MobileDialogState extends State { final phoneField = TextEditingController(); late UserProvider userProvider; - late UserApi api; + late Auth0UserApi api; late String channel; PhoneNumber number = PhoneNumber(isoCode: 'US'); diff --git a/lib/views/screens/advanced_security_screen.dart b/lib/views/screens/advanced_security_screen.dart index 945a53c..0c04c2a 100644 --- a/lib/views/screens/advanced_security_screen.dart +++ b/lib/views/screens/advanced_security_screen.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'dart:convert'; -import 'package:angeleno_project/controllers/api_implementation.dart'; +import 'package:angeleno_project/controllers/auth0_user_api_implementation.dart'; import 'package:angeleno_project/views/dialogs/mobile.dart'; import 'package:flutter/material.dart'; @@ -11,11 +11,11 @@ import '../dialogs/authenticator.dart'; class AdvancedSecurityScreen extends StatefulWidget { final UserProvider userProvider; - final UserApi userApi; + final Auth0UserApi auth0UserApi; const AdvancedSecurityScreen({ required this.userProvider, - required this.userApi, + required this.auth0UserApi, super.key }); @@ -25,7 +25,7 @@ class AdvancedSecurityScreen extends StatefulWidget { class _AdvancedSecurityState extends State { - late UserApi api; + late Auth0UserApi auth0UserApi; late UserProvider userProvider; late bool authenticatorEnabled = false; @@ -42,12 +42,12 @@ class _AdvancedSecurityState extends State { void initState() { super.initState(); userProvider = widget.userProvider; - api = widget.userApi; + auth0UserApi = widget.auth0UserApi; _authMethods = getAuthenticationMethods(); } Future getAuthenticationMethods() async { - await api.getAuthenticationMethods(userProvider.user!.userId) + await auth0UserApi.getAuthenticationMethods(userProvider.user!.userId) .then((final response) { final bool success = response.statusCode == HttpStatus.ok; if (success) { @@ -82,7 +82,7 @@ class _AdvancedSecurityState extends State { } void disableMFA(final String mfaAuthId, final String method) { - api.unenrollMFA({ + auth0UserApi.unenrollMFA({ 'authFactorId': mfaAuthId, 'userId': widget.userProvider.user!.userId }).then((final response) { @@ -174,7 +174,7 @@ class _AdvancedSecurityState extends State { builder: (final BuildContext context) => AuthenticatorDialog( userProvider: userProvider, - userApi: api + auth0UserApi: auth0UserApi ), ).then((final value) { if (value != null && value == HttpStatus.ok.toString()){ @@ -246,7 +246,7 @@ class _AdvancedSecurityState extends State { builder: ( final BuildContext context) => MobileDialog( userProvider: userProvider, - userApi: api, + userApi: auth0UserApi, channel: 'sms', ) ).then((final value) { @@ -283,7 +283,7 @@ class _AdvancedSecurityState extends State { builder: ( final BuildContext context) => MobileDialog( userProvider: userProvider, - userApi: api, + userApi: auth0UserApi, channel: 'voice', ) ).then((final value) { diff --git a/lib/views/screens/home_screen.dart b/lib/views/screens/home_screen.dart index 58d01fa..55483b3 100644 --- a/lib/views/screens/home_screen.dart +++ b/lib/views/screens/home_screen.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../controllers/api_implementation.dart'; +import '../../controllers/auth0_user_api_implementation.dart'; import '../../controllers/user_provider.dart'; import '../../models/user.dart'; @@ -19,6 +19,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { final GlobalKey scaffoldKey = GlobalKey(); + final Auth0UserApi auth0UserApi = Auth0UserApi(); late UserProvider userProvider; late User user; late OverlayProvider overlayProvider; @@ -101,11 +102,11 @@ class _MyHomePageState extends State { List get screens => [ const ProfileScreen(), PasswordScreen( - userApi: UserApi(), + auth0UserApi: auth0UserApi, ), AdvancedSecurityScreen( userProvider: userProvider, - userApi: UserApi(), + auth0UserApi: auth0UserApi, ) ]; diff --git a/lib/views/screens/password_screen.dart b/lib/views/screens/password_screen.dart index 7713f60..0dccfe0 100644 --- a/lib/views/screens/password_screen.dart +++ b/lib/views/screens/password_screen.dart @@ -5,15 +5,15 @@ import 'package:angeleno_project/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../controllers/api_implementation.dart'; +import '../../controllers/auth0_user_api_implementation.dart'; import '../../controllers/overlay_provider.dart'; import '../../controllers/user_provider.dart'; class PasswordScreen extends StatefulWidget { - final UserApi userApi; + final Auth0UserApi auth0UserApi; const PasswordScreen({ - required this.userApi, + required this.auth0UserApi, super.key }); @@ -24,7 +24,7 @@ class PasswordScreen extends StatefulWidget { class _PasswordScreenState extends State { late OverlayProvider overlayProvider; late UserProvider userProvider; - late UserApi api; + late Auth0UserApi auth0UserApi; final minPasswordLength = 12; @@ -45,7 +45,7 @@ class _PasswordScreenState extends State { @override void initState() { super.initState(); - api = widget.userApi; + auth0UserApi = widget.auth0UserApi; } void submitRequest() { @@ -64,7 +64,7 @@ class _PasswordScreenState extends State { userId: userProvider.user!.userId ); - api.updatePassword(body).then((final response) { + auth0UserApi.updatePassword(body).then((final response) { final success = response['status'] == HttpStatus.ok; overlayProvider.hideLoading(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/views/screens/profile_screen.dart b/lib/views/screens/profile_screen.dart index 3caf6ad..973ebfe 100644 --- a/lib/views/screens/profile_screen.dart +++ b/lib/views/screens/profile_screen.dart @@ -4,7 +4,7 @@ import 'package:angeleno_project/controllers/user_provider.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../controllers/api_implementation.dart'; +import '../../controllers/auth0_user_api_implementation.dart'; import '../../controllers/overlay_provider.dart'; import '../../models/user.dart'; @@ -20,7 +20,7 @@ class _ProfileScreenState extends State { late OverlayProvider overlayProvider; late UserProvider userProvider; late User user; - final api = UserApi(); + final auth0UserApi = Auth0UserApi(); @override void initState() { @@ -31,7 +31,7 @@ class _ProfileScreenState extends State { // Only submit patch if data has been updated if (!(user == userProvider.cleanUser)) { overlayProvider.showLoading(); - api.updateUser(user).then((final response) { + auth0UserApi.updateUser(user).then((final response) { final success = response == HttpStatus.ok; overlayProvider.hideLoading(); if (success) { diff --git a/test/advanced_test.dart b/test/advanced_test.dart index b3bd80f..dc2221b 100644 --- a/test/advanced_test.dart +++ b/test/advanced_test.dart @@ -1,4 +1,4 @@ -import 'package:angeleno_project/controllers/api_implementation.dart'; +import 'package:angeleno_project/controllers/auth0_user_api_implementation.dart'; import 'package:angeleno_project/controllers/user_provider.dart'; import 'package:angeleno_project/models/api_response.dart'; import 'package:angeleno_project/views/dialogs/mobile.dart'; @@ -8,9 +8,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'mocks/advanced_test.mocks.dart'; +import 'mocks/auth0_user_api_mock.dart'; -@GenerateNiceMocks([MockSpec()]) +@GenerateNiceMocks([MockSpec()]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -77,7 +77,7 @@ void main() { home: Scaffold( body: AdvancedSecurityScreen( userProvider: userProvider, - userApi: mockUserApi + auth0UserApi: mockUserApi ), ) ), diff --git a/test/auth0_api_test.dart b/test/auth0_api_test.dart index 5bc753c..4481839 100644 --- a/test/auth0_api_test.dart +++ b/test/auth0_api_test.dart @@ -3,7 +3,7 @@ import 'package:angeleno_project/models/user.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:mockito/mockito.dart'; -import 'mocks/advanced_test.mocks.dart'; +import 'mocks/auth0_user_api_mock.dart'; class MockClient extends Mock implements http.Client {} diff --git a/test/mocks/advanced_test.mocks.dart b/test/mocks/auth0_user_api_mock.dart similarity index 97% rename from test/mocks/advanced_test.mocks.dart rename to test/mocks/auth0_user_api_mock.dart index 42ff3fc..97bf5ef 100644 --- a/test/mocks/advanced_test.mocks.dart +++ b/test/mocks/auth0_user_api_mock.dart @@ -5,7 +5,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'package:angeleno_project/controllers/api_implementation.dart' as _i3; +import 'package:angeleno_project/controllers/auth0_user_api_implementation.dart' as _i3; import 'package:angeleno_project/models/api_response.dart' as _i2; import 'package:angeleno_project/models/password_reset.dart' as _i7; import 'package:angeleno_project/models/user.dart' as _i6; @@ -38,7 +38,7 @@ class _FakeApiResponse_0 extends _i1.SmartFake implements _i2.ApiResponse { /// A class which mocks [UserApi]. /// /// See the documentation for Mockito's code generation for more information. -class MockUserApi extends _i1.Mock implements _i3.UserApi { +class MockUserApi extends _i1.Mock implements _i3.Auth0UserApi { @override String get baseUrl => (super.noSuchMethod( Invocation.getter(#baseUrl), diff --git a/test/password_test.dart b/test/password_test.dart index 07a1dc6..ad2b0b9 100644 --- a/test/password_test.dart +++ b/test/password_test.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'mocks/advanced_test.mocks.dart'; +import 'mocks/auth0_user_api_mock.dart'; void main() { @@ -57,7 +57,7 @@ void main() { ], child: MaterialApp( home: Scaffold( - body: PasswordScreen(userApi: mockUserApi) + body: PasswordScreen(auth0UserApi: mockUserApi) ) ) ) From 2bc04a95c41f93c438910f89ae4d5b66ec956e3a Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 08:26:09 -0700 Subject: [PATCH 02/11] Uncomment flutter test from tests.bat --- tests.bat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests.bat b/tests.bat index ee198df..1e61293 100644 --- a/tests.bat +++ b/tests.bat @@ -1,7 +1,7 @@ @echo off -@REM call echo Running flutter tests -@REM call flutter test +call echo Running flutter tests +call flutter test call echo Running dart analyze call dart analyze From 232fbebeecb8872a2aee02c183bd788258033980 Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 09:42:56 -0700 Subject: [PATCH 03/11] Add timeout durations to all requests --- lib/controllers/auth0_user_api_implementation.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/controllers/auth0_user_api_implementation.dart b/lib/controllers/auth0_user_api_implementation.dart index 3d8f64f..91098c5 100644 --- a/lib/controllers/auth0_user_api_implementation.dart +++ b/lib/controllers/auth0_user_api_implementation.dart @@ -134,7 +134,7 @@ class Auth0UserApi extends Api { Uri.parse('/auth0/updatePassword'), headers: headers, body: reqBody - ); + ).timeout(const Duration(seconds: 5)); response = { 'status': request.statusCode, @@ -166,7 +166,7 @@ class Auth0UserApi extends Api { Uri.parse('/auth0/authMethods'), headers: headers, body: reqBody - ); + ).timeout(const Duration(seconds: 5)); if (request.statusCode == HttpStatus.ok) { return ApiResponse(request.statusCode, request.body); @@ -197,7 +197,7 @@ class Auth0UserApi extends Api { Uri.parse('/auth0/enrollMFA'), headers: headers, body: reqBody - ); + ).timeout(const Duration(seconds: 5)); final jsonBody = jsonDecode(request.body); final barcode = jsonBody['barcode_uri'] ?? ''; @@ -247,7 +247,7 @@ class Auth0UserApi extends Api { Uri.parse('/auth0/confirmMFA'), headers: headers, body: reqBody - ); + ).timeout(const Duration(seconds: 5)); if (request.statusCode == HttpStatus.ok) { return ApiResponse(request.statusCode, ''); @@ -276,7 +276,7 @@ class Auth0UserApi extends Api { Uri.parse('/auth0/unenrollMFA'), headers: headers, body: reqBody - ); + ).timeout(const Duration(seconds: 5)); if (request.statusCode == HttpStatus.ok) { return ApiResponse(request.statusCode, ''); From aab48fd123d6935dc9c64e8c2f10cec66f35bf7a Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 09:43:52 -0700 Subject: [PATCH 04/11] Pass instance to profile child --- lib/views/screens/home_screen.dart | 4 +++- lib/views/screens/profile_screen.dart | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/views/screens/home_screen.dart b/lib/views/screens/home_screen.dart index 55483b3..26c3c06 100644 --- a/lib/views/screens/home_screen.dart +++ b/lib/views/screens/home_screen.dart @@ -100,7 +100,9 @@ class _MyHomePageState extends State { } List get screens => [ - const ProfileScreen(), + ProfileScreen( + auth0UserApi: auth0UserApi, + ), PasswordScreen( auth0UserApi: auth0UserApi, ), diff --git a/lib/views/screens/profile_screen.dart b/lib/views/screens/profile_screen.dart index 973ebfe..49e4875 100644 --- a/lib/views/screens/profile_screen.dart +++ b/lib/views/screens/profile_screen.dart @@ -9,7 +9,12 @@ import '../../controllers/overlay_provider.dart'; import '../../models/user.dart'; class ProfileScreen extends StatefulWidget { - const ProfileScreen({super.key}); + final Auth0UserApi auth0UserApi; + + const ProfileScreen({ + required this.auth0UserApi, + super.key + }); @override State createState() => _ProfileScreenState(); @@ -17,14 +22,16 @@ class ProfileScreen extends StatefulWidget { class _ProfileScreenState extends State { final GlobalKey formKey = GlobalKey(); + late Auth0UserApi auth0UserApi; late OverlayProvider overlayProvider; late UserProvider userProvider; late User user; - final auth0UserApi = Auth0UserApi(); @override void initState() { super.initState(); + + auth0UserApi = widget.auth0UserApi; } void updateUser() { From 5e2853f9f6d0a6f4367e0d108d00e092d92cb815 Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 10:08:07 -0700 Subject: [PATCH 05/11] Update Advanced Security Screen & Dialogs - Autofocus and Enforce Phone Validation in non-test mode - Introduce a variable to determine if running flutter test - Dialog return values as ints, instead of type conversion - Implement full screen dialog on mobile view - Change phone number input country code to bottom sheet to prevent loading all flags on widget mount. Also had issues on mobile. --- lib/utils/constants.dart | 3 + lib/views/dialogs/authenticator.dart | 149 ++++++++++-------- lib/views/dialogs/mobile.dart | 67 +++++--- .../screens/advanced_security_screen.dart | 48 +++--- 4 files changed, 150 insertions(+), 117 deletions(-) diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index da50370..e448efe 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -9,6 +9,9 @@ const cloudFunctionURL = const serviceAccountSecret = String.fromEnvironment('SA_SECRET_KEY'); const serviceAccountEmail = String.fromEnvironment('SA_EMAIL'); +/* Media Query Breakpoints */ +const double smallScreen = 430; + /* Colors */ ColorScheme colorScheme = const ColorScheme( brightness: Brightness.light, diff --git a/lib/views/dialogs/authenticator.dart b/lib/views/dialogs/authenticator.dart index cc1b74c..f25558f 100644 --- a/lib/views/dialogs/authenticator.dart +++ b/lib/views/dialogs/authenticator.dart @@ -125,7 +125,7 @@ class _AuthenticatorDialogState extends State { auth0UserApi.confirmMFA(body).then((final response) { if (response.statusCode == HttpStatus.ok) { - Navigator.pop(context, response.statusCode.toString()); + Navigator.pop(context, response.statusCode); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( behavior: SnackBarBehavior.floating, width: 280.0, @@ -262,56 +262,56 @@ class _AuthenticatorDialogState extends State { ); Widget get confirmationScreen => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - dialogClose, - TextButton( - onPressed: () { - confirmTOTP(); - }, - child: const Text('Finish'), - ) - ], - ), - Expanded( - child: Align( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Enter code displayed from the application:', - textAlign: TextAlign.center, - softWrap: true - ), - SizedBox( - width: 250, - child: TextFormField( - key: const Key('totpCode'), - autofocus: true, - autovalidateMode: AutovalidateMode.always, - validator: (final value) { - if (value == null || value.trim().isEmpty) { - return 'Code is required'; - } - return null; - }, - onChanged: (final val) { - setState(() { - totpCode = val; - }); - }, - ) - ), - const SizedBox(height: 15), - if (errMsg.isNotEmpty) - Text(errMsg, style: TextStyle(color: colorScheme.error)) - ], - ) - ) - ) - ] + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + dialogClose, + TextButton( + onPressed: () { + confirmTOTP(); + }, + child: const Text('Finish'), + ) + ], + ), + Expanded( + child: Align( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Enter code displayed from the application:', + textAlign: TextAlign.center, + softWrap: true + ), + SizedBox( + width: 250, + child: TextFormField( + key: const Key('totpCode'), + autofocus: true, + autovalidateMode: AutovalidateMode.always, + validator: (final value) { + if (value == null || value.trim().isEmpty) { + return 'Code is required'; + } + return null; + }, + onChanged: (final val) { + setState(() { + totpCode = val; + }); + }, + ) + ), + const SizedBox(height: 15), + if (errMsg.isNotEmpty) + Text(errMsg, style: TextStyle(color: colorScheme.error)) + ], + ) + ) + ) + ] ); List get screens => [ @@ -320,25 +320,36 @@ class _AuthenticatorDialogState extends State { confirmationScreen ]; - @override - Widget build(final BuildContext context) => Dialog( - insetPadding: EdgeInsets.zero, - child: SizedBox( - width: double.infinity, - height: double.infinity, - child: PageView.builder( - controller: _pageController, - itemCount: 3, - onPageChanged: (final index) { - setState(() { - _pageIndex++; - }); - }, - itemBuilder: (final context, final index) => Container( + Widget get dialogBody => SizedBox( + width: double.infinity, + height: double.infinity, + child: PageView.builder( + controller: _pageController, + itemCount: 3, + onPageChanged: (final index) { + setState(() { + _pageIndex++; + }); + }, + itemBuilder: (final context, final index) => Container( padding: const EdgeInsets.all(20), child: screens[_pageIndex] - ) - ), - ) + ) + ), ); + + @override + Widget build(final BuildContext context) { + final double screenWidth = MediaQuery.of(context).size.width; + final bool isSmallScreen = screenWidth < smallScreen; + + return isSmallScreen ? + Dialog.fullscreen( + child: dialogBody + ) + : + Dialog( + child: dialogBody + ); + } } \ No newline at end of file diff --git a/lib/views/dialogs/mobile.dart b/lib/views/dialogs/mobile.dart index b76c15e..1854432 100644 --- a/lib/views/dialogs/mobile.dart +++ b/lib/views/dialogs/mobile.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:angeleno_project/controllers/auth0_user_api_implementation.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl_phone_number_input/intl_phone_number_input.dart'; @@ -32,6 +33,9 @@ class _MobileDialogState extends State { late Auth0UserApi api; late String channel; + final isNotTestMode = kIsWeb || + !Platform.environment.containsKey('FLUTTER_TEST'); + PhoneNumber number = PhoneNumber(isoCode: 'US'); String initialCountry = 'US'; @@ -126,7 +130,7 @@ class _MobileDialogState extends State { api.confirmMFA(body).then((final response) { if (response.statusCode == HttpStatus.ok) { - Navigator.pop(context, response.statusCode.toString()); + Navigator.pop(context, response.statusCode); ScaffoldMessenger.of(context).showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, width: 280.0, @@ -146,8 +150,7 @@ class _MobileDialogState extends State { TextButton( onPressed: () { try { - if (!validPhoneNumber && !Platform.environment.containsKey('FLUTTER_TEST')) { - setState(() => errMsg = 'Invalid phone number'); + if (!validPhoneNumber && isNotTestMode) { return; } _navigateToNextPage(); @@ -171,6 +174,10 @@ class _MobileDialogState extends State { SizedBox( width: 500, child: InternationalPhoneNumberInput( + autoFocus: isNotTestMode, + selectorConfig: const SelectorConfig( + selectorType: PhoneInputSelectorType.BOTTOM_SHEET + ), key: const Key('phoneField'), onInputChanged: (final PhoneNumber number) { phoneNumber = number.phoneNumber!; @@ -178,6 +185,8 @@ class _MobileDialogState extends State { onInputValidated: (final bool value) { validPhoneNumber = value; }, + autoValidateMode: isNotTestMode ? + AutovalidateMode.always : AutovalidateMode.disabled, selectorTextStyle: const TextStyle(color: Colors.black), initialValue: number, textFieldController: phoneField, @@ -187,9 +196,7 @@ class _MobileDialogState extends State { ), inputBorder: const OutlineInputBorder() ), - ), - if (errMsg.isNotEmpty) - Text(errMsg, style: TextStyle(color: colorScheme.error)) + ) ], ), ), @@ -323,24 +330,36 @@ class _MobileDialogState extends State { codeScreen ]; - @override - Widget build(final BuildContext context) => Dialog( - child: SizedBox( - width: double.infinity, - height: double.infinity, - child: PageView.builder( - controller: _pageController, - itemCount: 3, - onPageChanged: (final index) { - setState(() { - _pageIndex++; - }); - }, - itemBuilder: (final context, final index) => Container( - padding: const EdgeInsets.all(20), - child: screens[_pageIndex], - ) - ), + Widget get dialogBody => SizedBox( + width: double.infinity, + height: double.infinity, + child: PageView.builder( + controller: _pageController, + itemCount: 3, + onPageChanged: (final index) { + setState(() { + _pageIndex++; + }); + }, + itemBuilder: (final context, final index) => Container( + padding: const EdgeInsets.all(20), + child: screens[_pageIndex], + ) ), ); + + @override + Widget build(final BuildContext context) { + final double screenWidth = MediaQuery.of(context).size.width; + final bool isSmallScreen = screenWidth < smallScreen; + + return isSmallScreen ? + Dialog.fullscreen( + child: dialogBody + ) + : + Dialog( + child: dialogBody + ); + } } \ No newline at end of file diff --git a/lib/views/screens/advanced_security_screen.dart b/lib/views/screens/advanced_security_screen.dart index 0c04c2a..0b07037 100644 --- a/lib/views/screens/advanced_security_screen.dart +++ b/lib/views/screens/advanced_security_screen.dart @@ -88,7 +88,7 @@ class _AdvancedSecurityState extends State { }).then((final response) { final bool success = response.statusCode == HttpStatus.ok; if (success) { - Navigator.pop(context, response.statusCode.toString()); + Navigator.pop(context, response.statusCode); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( behavior: SnackBarBehavior.floating, width: 280.0, @@ -127,7 +127,7 @@ class _AdvancedSecurityState extends State { authenticatorEnabled ? FilledButton.tonal( key: const Key('disableAuthenticator'), - onPressed: () => showDialog( + onPressed: () => showDialog( context: context, builder: (final BuildContext context) => AlertDialog( title: const Text('Remove authenticator app?'), @@ -145,7 +145,7 @@ class _AdvancedSecurityState extends State { TextButton( child: const Text('Cancel'), onPressed: () { - Navigator.pop(context, ''); + Navigator.pop(context); }, ), TextButton( @@ -157,7 +157,7 @@ class _AdvancedSecurityState extends State { ], ) ).then((final value) { - if (value != null && value == HttpStatus.ok.toString()) { + if (value != null && value == HttpStatus.ok) { setState(() { authenticatorEnabled = false; }); @@ -169,15 +169,15 @@ class _AdvancedSecurityState extends State { FilledButton( key: const Key('enableAuthenticator'), onPressed: () { - showDialog( + showDialog( context: context, builder: (final BuildContext context) => - AuthenticatorDialog( - userProvider: userProvider, - auth0UserApi: auth0UserApi - ), + AuthenticatorDialog( + userProvider: userProvider, + auth0UserApi: auth0UserApi + ), ).then((final value) { - if (value != null && value == HttpStatus.ok.toString()){ + if (value != null && value == HttpStatus.ok){ setState(() { authenticatorEnabled = true; }); @@ -198,7 +198,7 @@ class _AdvancedSecurityState extends State { smsEnabled ? FilledButton.tonal( key: const Key('disableSMS'), - onPressed: () => showDialog( + onPressed: () => showDialog( context: context, builder: (final BuildContext context) => AlertDialog( title: const Text('Remove SMS MFA?'), @@ -217,7 +217,7 @@ class _AdvancedSecurityState extends State { TextButton( child: const Text('Cancel'), onPressed: () { - Navigator.pop(context, ''); + Navigator.pop(context); }, ), TextButton( @@ -229,7 +229,7 @@ class _AdvancedSecurityState extends State { ], ) ).then((final value) { - if (value != null && value == HttpStatus.ok.toString()) { + if (value != null && value == HttpStatus.ok) { setState(() { smsEnabled = false; }); @@ -241,7 +241,7 @@ class _AdvancedSecurityState extends State { FilledButton( key: const Key('enableSMS'), onPressed: () { - showDialog( + showDialog( context: context, builder: ( final BuildContext context) => MobileDialog( @@ -250,7 +250,7 @@ class _AdvancedSecurityState extends State { channel: 'sms', ) ).then((final value) { - if (value != null && value == HttpStatus.ok.toString()) { + if (value != null && value == HttpStatus.ok) { setState(() { smsEnabled = true; }); @@ -278,16 +278,16 @@ class _AdvancedSecurityState extends State { FilledButton( key: const Key('enableVoice'), onPressed: () { - showDialog( - context: context, - builder: ( - final BuildContext context) => MobileDialog( - userProvider: userProvider, - userApi: auth0UserApi, - channel: 'voice', - ) + showDialog( + context: context, + builder: ( + final BuildContext context) => MobileDialog( + userProvider: userProvider, + userApi: auth0UserApi, + channel: 'voice', + ) ).then((final value) { - if (value != null && value == HttpStatus.ok.toString()) { + if (value != null && value == HttpStatus.ok) { setState(() { voiceEnabled = true; }); From dc191a81d09395e2baf41b2c184f68a9301c0856 Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 11:51:59 -0700 Subject: [PATCH 06/11] Accessibility for Disabled Inputs on Profile Screen Increases the contrast for disabled inputs from 1.2:1 to 4.25:1, recommended contrast is 3:1 to meet accessibility standards --- lib/main.dart | 8 ++- lib/utils/constants.dart | 3 ++ lib/views/screens/profile_screen.dart | 72 +++++++++++++++------------ 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 939ba6a..49ae7cd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,13 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, - colorScheme: colorScheme + colorScheme: colorScheme, + inputDecorationTheme: const InputDecorationTheme( + disabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: disabledColor) + ), + + ) ), onGenerateRoute: (final settings) { final uri = Uri.parse(settings.name!); diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index e448efe..3c0f69e 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -13,6 +13,9 @@ const serviceAccountEmail = String.fromEnvironment('SA_EMAIL'); const double smallScreen = 430; /* Colors */ + +const disabledColor = Colors.black54; + ColorScheme colorScheme = const ColorScheme( brightness: Brightness.light, primary: Color(0xFF006B59), diff --git a/lib/views/screens/profile_screen.dart b/lib/views/screens/profile_screen.dart index 49e4875..a664305 100644 --- a/lib/views/screens/profile_screen.dart +++ b/lib/views/screens/profile_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:angeleno_project/controllers/user_provider.dart'; +import 'package:angeleno_project/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -59,6 +60,16 @@ class _ProfileScreenState extends State { } } + InputDecoration inputDecoration (final String label, final bool editMode) => + InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + labelStyle: TextStyle(color: editMode ? null : disabledColor), + ); + + TextStyle textStyle (final bool editMode) => + TextStyle(color: editMode ? null : disabledColor); + @override Widget build(final BuildContext context) { overlayProvider = context.watch(); @@ -70,6 +81,8 @@ class _ProfileScreenState extends State { user = userProvider.user!; } + final editMode = userProvider.isEditing; + return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -88,7 +101,7 @@ class _ProfileScreenState extends State { children: [ ElevatedButton( onPressed: () { - if (userProvider.isEditing) { + if (editMode) { updateUser(); } setState(() { @@ -96,16 +109,15 @@ class _ProfileScreenState extends State { }); }, child: Text( - userProvider.isEditing ? 'Save' : 'Edit' + editMode ? 'Save' : 'Edit' ), ) ]), const SizedBox(height: 25.0), TextFormField( - enabled: userProvider.isEditing, - decoration: const InputDecoration( - labelText: 'First Name', - border: OutlineInputBorder()), + enabled: editMode, + decoration: inputDecoration('First Name', editMode), + style: textStyle(editMode), initialValue: user.firstName, keyboardType: TextInputType.name, onChanged: (final val) { @@ -114,10 +126,9 @@ class _ProfileScreenState extends State { ), const SizedBox(height: 25.0), TextFormField( - enabled: userProvider.isEditing, - decoration: const InputDecoration( - labelText: 'Last Name', - border: OutlineInputBorder()), + enabled: editMode, + decoration: inputDecoration('Last Name', editMode), + style: textStyle(editMode), initialValue: user.lastName, keyboardType: TextInputType.name, onChanged: (final val) { @@ -126,10 +137,9 @@ class _ProfileScreenState extends State { ), const SizedBox(height: 25.0), TextFormField( - enabled: userProvider.isEditing, - decoration: const InputDecoration( - labelText: 'Mobile', - border: OutlineInputBorder()), + enabled: editMode, + decoration: inputDecoration('Mobile', editMode), + style: textStyle(editMode), initialValue: user.phone, onChanged: (final val) { user.phone = val; @@ -137,10 +147,9 @@ class _ProfileScreenState extends State { ), const SizedBox(height: 25.0), TextFormField( - enabled: userProvider.isEditing, - decoration: const InputDecoration( - labelText: 'Address', - border: OutlineInputBorder()), + enabled: editMode, + decoration: inputDecoration('Address', editMode), + style: textStyle(editMode), keyboardType: TextInputType.streetAddress, initialValue: user.address, onChanged: (final val) { @@ -149,10 +158,9 @@ class _ProfileScreenState extends State { ), const SizedBox(height: 25.0), TextFormField( - enabled: userProvider.isEditing, - decoration: const InputDecoration( - labelText: 'Address 2', - border: OutlineInputBorder()), + enabled: editMode, + decoration: inputDecoration('Address 2', editMode), + style: textStyle(editMode), keyboardType: TextInputType.streetAddress, initialValue: user.address2, onChanged: (final val) { @@ -161,10 +169,9 @@ class _ProfileScreenState extends State { ), const SizedBox(height: 25.0), TextFormField( - enabled: userProvider.isEditing, - decoration: const InputDecoration( - labelText: 'City', - border: OutlineInputBorder()), + enabled: editMode, + decoration: inputDecoration('City', editMode), + style: textStyle(editMode), keyboardType: TextInputType.streetAddress, initialValue: user.city, onChanged: (final val) { @@ -173,10 +180,9 @@ class _ProfileScreenState extends State { ), const SizedBox(height: 25.0), TextFormField( - enabled: userProvider.isEditing, - decoration: const InputDecoration( - labelText: 'State', - border: OutlineInputBorder()), + enabled: editMode, + decoration: inputDecoration('State', editMode), + style: textStyle(editMode), keyboardType: TextInputType.streetAddress, initialValue: user.state, onChanged: (final val) { @@ -185,9 +191,9 @@ class _ProfileScreenState extends State { ), const SizedBox(height: 25.0), TextFormField( - enabled: userProvider.isEditing, - decoration: const InputDecoration( - labelText: 'Zip', border: OutlineInputBorder()), + enabled: editMode, + decoration: inputDecoration('Zip', editMode), + style: textStyle(editMode), initialValue: user.zip, onChanged: (final val) { user.zip = val; From db9e8657be0bb46fa64c179c566696d797b7d803 Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 13:47:10 -0700 Subject: [PATCH 07/11] Standardize errors; check for required body parameters --- functions/auth0/api/auth0.js | 65 ++++++++++++++++++++++++++++------ functions/auth0/utils/auth0.js | 2 +- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/functions/auth0/api/auth0.js b/functions/auth0/api/auth0.js index d56c641..867c6b7 100644 --- a/functions/auth0/api/auth0.js +++ b/functions/auth0/api/auth0.js @@ -81,8 +81,15 @@ const updateUser = onRequest(async (req, res) => { return res.status(200).send(); } catch (err) { - console.error(JSON.stringify(err)); - return res.status(err?.response?.status || 500).send(err.message); + console.error(err); + + const { + status = 500, + message = '', + data: {error_description}, + } = err.response; + + return res.status(status).send(message || error_description); } }); @@ -98,7 +105,7 @@ const updatePassword = onRequest(async (req, res) => { if (!email.length || !oldPassword.length || !newPassword.length || !userId.length) { - res.status(400).send('Invalid request'); + res.status(400).send('Invalid request - missing required fields.'); return; } @@ -127,7 +134,7 @@ const updatePassword = onRequest(async (req, res) => { console.error(`Error: ${err.message}`); const { - status, + status = 500, data: {error_description, message}, } = err?.response; @@ -141,13 +148,20 @@ const updatePassword = onRequest(async (req, res) => { const authMethods = onRequest(async (req, res) => { const body = req.body; + const {userId} = body; + + if (!userId) { + res.status(400).send('Invalid request - missing required fields.'); + return; + } + try { const auth0Token = await getAccessToken(); const config = { method: 'get', maxBodyLength: Infinity, - url: `https://${auth0Domain}/api/v2/users/${body.userId}/authentication-methods`, + url: `https://${auth0Domain}/api/v2/users/${userId}/authentication-methods`, headers: { Accept: 'application/json', Authorization: `Bearer ${auth0Token}`, @@ -158,6 +172,13 @@ const authMethods = onRequest(async (req, res) => { res.status(200).send(request.data); } catch (err) { console.error(err); + + const { + status = 500, + message = '', + } = err.response; + + return res.status(status).send(message); } }); @@ -209,14 +230,17 @@ const enrollMFA = onRequest(async (req, res) => { } catch (err) { console.error(err); - let {code, message} = err; + let { + code = 500, + message + } = err; // Status Code for failed Authorization if (code === 403) { message = 'Invalid Password.'; } - res.status(code || 500).send({error: message || 'Error encountered'}); + res.status(code).send({error: message || 'Error encountered'}); } }); @@ -229,6 +253,11 @@ const confirmMFA = onRequest(async (req, res) => { oobCode = '', } = body; + if (!mfaToken) { + res.status(400).send('Invalid request - missing required fields.'); + return; + } + try { let additionalData = {}; @@ -263,7 +292,8 @@ const confirmMFA = onRequest(async (req, res) => { let customError = ''; const { - status, + status = 500, + message = '', data: {error_description}, } = err?.response; @@ -271,20 +301,28 @@ const confirmMFA = onRequest(async (req, res) => { customError = 'Invalid code.'; } - res.status(status).send({error: customError || error_description}); + res.status(status).send({error: message || customError || error_description}); } }); const unenrollMFA = onRequest(async (req, res) => { const body = req.body; + const { userId, authFactorId } = body; + + if (!userId || !authFactorId) { + res.status(400).send('Invalid request - missing required fields.'); + return; + + } + try { const auth0Token = await getAccessToken(); const config = { method: 'delete', maxBodyLength: Infinity, - url: `https://${auth0Domain}/api/v2/users/${body.userId}/authentication-methods/${body.authFactorId}`, + url: `https://${auth0Domain}/api/v2/users/${userId}/authentication-methods/${authFactorId}`, headers: { 'Authorization': `Bearer ${auth0Token}`, }, @@ -294,6 +332,13 @@ const unenrollMFA = onRequest(async (req, res) => { res.status(200).send(request.data); } catch (err) { console.error(err); + + const { + status = 500, + message = '', + } = err.response; + + return res.status(status).send(message); } }); diff --git a/functions/auth0/utils/auth0.js b/functions/auth0/utils/auth0.js index 920d351..acb8c2b 100644 --- a/functions/auth0/utils/auth0.js +++ b/functions/auth0/utils/auth0.js @@ -43,7 +43,7 @@ const getAccessToken = async () => { return auth0Token; } catch (err) { console.error(err); - throw new Error('Error getting access token'); + throw err; } }; From acc4712a21bd633e40e33d060fb31a9e96e51451 Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 13:57:37 -0700 Subject: [PATCH 08/11] Remove print statement --- lib/controllers/auth0_user_api_implementation.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/controllers/auth0_user_api_implementation.dart b/lib/controllers/auth0_user_api_implementation.dart index 91098c5..18f0f71 100644 --- a/lib/controllers/auth0_user_api_implementation.dart +++ b/lib/controllers/auth0_user_api_implementation.dart @@ -84,7 +84,6 @@ class Auth0UserApi extends Api { try { final token = await getOAuthToken(); - print(token); if (token.isEmpty) { throw const FormatException('Empty token received'); From 2de687e4b262d1d20eb69d397ba954eef5a85cb7 Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 14:31:17 -0700 Subject: [PATCH 09/11] Add Authentication to all methods --- .../auth0_user_api_implementation.dart | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/controllers/auth0_user_api_implementation.dart b/lib/controllers/auth0_user_api_implementation.dart index 18f0f71..6d6b762 100644 --- a/lib/controllers/auth0_user_api_implementation.dart +++ b/lib/controllers/auth0_user_api_implementation.dart @@ -122,8 +122,11 @@ class Auth0UserApi extends Api { Future> updatePassword(final PasswordBody body) async { late Map response; + final token = await getOAuthToken(); + final headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' }; final reqBody = json.encode(body); @@ -154,8 +157,11 @@ class Auth0UserApi extends Api { @override Future getAuthenticationMethods(final String userId) async { + final token = await getOAuthToken(); + final headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' }; final reqBody = json.encode({'userId': userId}); @@ -185,8 +191,11 @@ class Auth0UserApi extends Api { enrollMFA(final Map body) async { late Map response; + final token = await getOAuthToken(); + final headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' }; final reqBody = json.encode(body); @@ -235,8 +244,11 @@ class Auth0UserApi extends Api { @override Future confirmMFA(final Map body) async { + final token = await getOAuthToken(); + final headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' }; final reqBody = json.encode(body); @@ -264,8 +276,11 @@ class Auth0UserApi extends Api { @override Future unenrollMFA(final Map body) async { + final token = await getOAuthToken(); + final headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' }; final reqBody = json.encode(body); From 528d239fc9691cc5f0eb96668d91a91c20eeadc4 Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 14:46:07 -0700 Subject: [PATCH 10/11] Simplify route function --- lib/main.dart | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 49ae7cd..28d9bb2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,17 +37,10 @@ class MyApp extends StatelessWidget { ) ), - onGenerateRoute: (final settings) { - final uri = Uri.parse(settings.name!); - - if (uri.path == '/' && uri.queryParameters.isNotEmpty) { - return MaterialPageRoute(builder: (final context) => const MyHomePage(), - settings: const RouteSettings(name: '/')); - } - - return MaterialPageRoute(builder: (final context) => const MyHomePage(), - settings: const RouteSettings(name: '/')); - }, + onGenerateRoute: (final settings) => MaterialPageRoute( + builder: (final context) => const MyHomePage(), + settings: const RouteSettings(name: '/') + ), home: const MyHomePage() ); } \ No newline at end of file From 4ee5cbe30b9d84dc682b00ca44b4024938cd749a Mon Sep 17 00:00:00 2001 From: Cristian Hernandez Date: Thu, 4 Apr 2024 15:25:22 -0700 Subject: [PATCH 11/11] Add Password Obscure tests --- lib/views/dialogs/authenticator.dart | 2 ++ lib/views/dialogs/mobile.dart | 4 +--- test/advanced_test.dart | 32 ++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/views/dialogs/authenticator.dart b/lib/views/dialogs/authenticator.dart index f25558f..c5a223b 100644 --- a/lib/views/dialogs/authenticator.dart +++ b/lib/views/dialogs/authenticator.dart @@ -168,6 +168,7 @@ class _AuthenticatorDialogState extends State { ), const SizedBox(height: 15), SizedBox( + key: const Key('passwordField'), width: 250, child: TextFormField( autofocus: true, @@ -187,6 +188,7 @@ class _AuthenticatorDialogState extends State { }, decoration: InputDecoration( suffixIcon: IconButton( + key: const Key('toggle_password'), onPressed: () { setState(() { obscurePassword = !obscurePassword; diff --git a/lib/views/dialogs/mobile.dart b/lib/views/dialogs/mobile.dart index 1854432..35edd3f 100644 --- a/lib/views/dialogs/mobile.dart +++ b/lib/views/dialogs/mobile.dart @@ -237,9 +237,6 @@ class _MobileDialogState extends State { child: TextFormField( autofocus: true, controller: passwordField, - onFieldSubmitted: (final value) { - - }, obscureText: obscurePassword, enableSuggestions: false, autocorrect: false, @@ -252,6 +249,7 @@ class _MobileDialogState extends State { }, decoration: InputDecoration( suffixIcon: IconButton( + key: const Key('toggle_password'), onPressed: () { setState(() { obscurePassword = !obscurePassword; diff --git a/test/advanced_test.dart b/test/advanced_test.dart index dc2221b..eca9aaf 100644 --- a/test/advanced_test.dart +++ b/test/advanced_test.dart @@ -121,6 +121,22 @@ void main() { await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField), 'userPassword'); + + final authenticatorPasswordFinder = find.descendant( + of: find.byKey(const Key('passwordField')), + matching: find.byType(TextField), + ); + + final authenticatorPasswordField = tester.firstWidget(authenticatorPasswordFinder); + expect(authenticatorPasswordField.obscureText, true); + + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('toggle_password'))); + await tester.pump(); + // ignore: lines_longer_than_80_chars + final refreshAuthenticatorPasswordField = tester.firstWidget(authenticatorPasswordFinder); + expect(refreshAuthenticatorPasswordField.obscureText, false); + await tester.tap(find.widgetWithText(TextButton, 'Continue')); await tester.pumpAndSettle(); @@ -175,6 +191,22 @@ void main() { await tester.pumpAndSettle(); await tester.enterText(find.byKey(const Key('passwordField')), 'myPassword'); + + final phonePasswordFinder = find.descendant( + of: find.byKey(const Key('passwordField')), + matching: find.byType(TextField), + ); + + final phonePasswordField = tester.firstWidget(phonePasswordFinder); + expect(phonePasswordField.obscureText, true); + + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('toggle_password'))); + await tester.pump(); + // ignore: lines_longer_than_80_chars + final refreshPhonePasswordField = tester.firstWidget(phonePasswordFinder); + expect(refreshPhonePasswordField.obscureText, false); + await tester.tap(find.widgetWithText(TextButton, 'Continue')); await tester.pumpAndSettle();