diff --git a/modules/authentication/src/Authentication.ts b/modules/authentication/src/Authentication.ts index 3cc870c59..e90d3948c 100644 --- a/modules/authentication/src/Authentication.ts +++ b/modules/authentication/src/Authentication.ts @@ -1,21 +1,22 @@ import { ManagedModule, ConfigController, - DatabaseProvider -} from "@conduitplatform/grpc-sdk"; + DatabaseProvider, +} from '@conduitplatform/grpc-sdk'; -import path from "path"; -import { isNil } from "lodash"; -import { status } from "@grpc/grpc-js"; +import path from 'path'; +import { isNil } from 'lodash'; +import { status } from '@grpc/grpc-js'; import AppConfigSchema from './config'; import { AdminHandlers } from './admin/admin'; import { AuthenticationRoutes } from './routes/routes'; import * as models from './models'; -import { ISignTokenOptions } from "./interfaces/ISignTokenOptions"; -import { AuthUtils } from "./utils/auth"; -import { TokenType } from "./constants/TokenType"; -import { v4 as uuid } from "uuid"; -import moment from "moment"; +import { ISignTokenOptions } from './interfaces/ISignTokenOptions'; +import { AuthUtils } from './utils/auth'; +import { TokenType } from './constants/TokenType'; +import { v4 as uuid } from 'uuid'; +import moment from 'moment'; +import { migrateLocalAuthConfig } from './migrations/localAuthConfig.migration'; export default class Authentication extends ManagedModule { config = AppConfigSchema; @@ -42,6 +43,7 @@ export default class Authentication extends ManagedModule { async onServerStart() { await this.grpcSdk.waitForExistence('database'); this.database = this.grpcSdk.databaseProvider!; + await migrateLocalAuthConfig(this.grpcSdk); } async onRegister() { @@ -120,7 +122,16 @@ export default class Authentication extends ManagedModule { async userCreate(call: any, callback: any) { const email = call.request.email; let password = call.request.password; - let verify = call.request.verify; + const verify = call.request.verify; + + const verificationConfig = ConfigController.getInstance().config.local.verification; + if (verify && !(verificationConfig.required && verificationConfig.send_email)) { + return callback({ + code: status.INVALID_ARGUMENT, + message: 'Email verification is disabled. Configuration required.', + }); + } + if (isNil(password) || password.length === 0) { password = AuthUtils.randomToken(8); } @@ -129,10 +140,10 @@ export default class Authentication extends ManagedModule { if (user) { return callback({ code: status.ALREADY_EXISTS, message: 'User already exists' }); } - if (email.indexOf('+') !== -1) { + if (AuthUtils.invalidEmailAddress(email)) { return callback({ code: status.INVALID_ARGUMENT, - message: 'Email contains unsupported characters', + message: 'Invalid email address provided', }); } const hashedPassword = await AuthUtils.hashPassword(password); diff --git a/modules/authentication/src/admin/admin.ts b/modules/authentication/src/admin/admin.ts index 5eefd398a..dea91c61c 100644 --- a/modules/authentication/src/admin/admin.ts +++ b/modules/authentication/src/admin/admin.ts @@ -11,7 +11,6 @@ import ConduitGrpcSdk, { ConduitNumber, ConduitBoolean, TYPE, - ConfigController, } from '@conduitplatform/grpc-sdk'; import { status} from '@grpc/grpc-js'; import { isNil } from 'lodash'; @@ -79,7 +78,7 @@ export class AdminHandlers { path: '/users', action: ConduitRouteActions.POST, bodyParams: { - identification: ConduitString.Required, + email: ConduitString.Required, password: ConduitString.Required, }, }, @@ -241,14 +240,13 @@ export class AdminHandlers { query[provider] = { $exists: true, $ne: null }; } } - let identifier; if (!isNil(search)) { if (search.match(/^[a-fA-F0-9]{24}$/)) { query = { _id : search } } else { - identifier = escapeStringRegexp(search); - query['email'] = { $regex: `.*${identifier}.*`, $options: 'i' }; + const emailIdentifier = escapeStringRegexp(search); + query['email'] = { $regex: `.*${emailIdentifier}.*`, $options: 'i' }; } } @@ -265,19 +263,13 @@ export class AdminHandlers { } async createUser(call: ParsedRouterRequest): Promise { - let { identification, password } = call.request.params; - - const config = ConfigController.getInstance().config; - if (config.local.identifier === 'email') { - if (identification.indexOf('+') !== -1) { - throw new GrpcError(status.INVALID_ARGUMENT, 'Email contains unsupported characters'); - } - - identification = identification.toLowerCase(); + let { email, password } = call.request.params; + if (AuthUtils.invalidEmailAddress(email)) { + throw new GrpcError(status.INVALID_ARGUMENT, 'Invalid email address provided'); } let user: User | null = await User.getInstance().findOne({ - email: identification, + email: email.toLowerCase(), }); if (!isNil(user)) { throw new GrpcError(status.ALREADY_EXISTS, 'User already exists'); @@ -285,7 +277,7 @@ export class AdminHandlers { let hashedPassword = await AuthUtils.hashPassword(password); user = await User.getInstance().create({ - email: identification, + email, hashedPassword, isVerified: true, }); diff --git a/modules/authentication/src/config/config.ts b/modules/authentication/src/config/config.ts index ed57e33e5..d0c0f2232 100644 --- a/modules/authentication/src/config/config.ts +++ b/modules/authentication/src/config/config.ts @@ -4,31 +4,26 @@ export default { default: true, }, local: { - identifier: { - doc: - 'The field name to use for id for a user logging in with local strategy ex. email/username', - format: 'String', - default: 'email', - }, enabled: { doc: 'Defines if this strategy is active or not', format: 'Boolean', default: true, }, - sendVerificationEmail: { - doc: - 'Defines if the authenticator should automatically send a verification e-mail to the user', - format: 'Boolean', - default: true, - }, - verificationRequired: { - doc: 'Defines if email verification is required for login', - format: 'Boolean', - default: true, - }, - verification_redirect_uri: { - format: 'String', - default: '', + verification: { + required: { + doc: 'Defines if email verification is required for login', + format: 'Boolean', + default: false, + }, + send_email: { + doc: 'Defines if the authenticator should automatically send a verification e-mail to the user', + format: 'Boolean', + default: false, + }, + redirect_uri: { + format: 'String', + default: '', + }, }, forgot_password_redirect_uri: { format: 'String', diff --git a/modules/authentication/src/handlers/facebook/facebook.ts b/modules/authentication/src/handlers/facebook/facebook.ts index 6f6bde151..62efb9296 100644 --- a/modules/authentication/src/handlers/facebook/facebook.ts +++ b/modules/authentication/src/handlers/facebook/facebook.ts @@ -31,21 +31,16 @@ export class FacebookHandlers extends OAuth2 { user_age_range: 'age_range', user_likes: 'likes', }; - this.defaultScopes = ["public_profile","email"]; - + this.defaultScopes = ['public_profile', 'email']; } async makeFields(scopes: string[]): Promise { - - let mappedScopes = scopes.map((scope: any) => { + return scopes.map((scope: any) => { return this.mapScopes[scope]; }).join(','); - - return mappedScopes; } async constructScopes(scopes: string[]): Promise { - return scopes.join(','); } diff --git a/modules/authentication/src/handlers/local.ts b/modules/authentication/src/handlers/local.ts index 7286b7942..d69de9b62 100644 --- a/modules/authentication/src/handlers/local.ts +++ b/modules/authentication/src/handlers/local.ts @@ -19,9 +19,8 @@ import moment = require('moment'); export class LocalHandlers { private emailModule: Email; - private sms: SMS; + private smsModule: SMS; private initialized: boolean = false; - private identifier: string = 'email'; constructor(private readonly grpcSdk: ConduitGrpcSdk) { } @@ -29,15 +28,16 @@ export class LocalHandlers { async validate(): Promise { const config = ConfigController.getInstance().config; let promise: Promise; - this.identifier = config.local.identifier; - if (this.identifier !== 'username') { - promise = this.grpcSdk.config.get('email').then((emailConfig: any) => { - if (!emailConfig.active) { - throw ConduitError.forbidden( - 'Cannot use local authentication without email module being enabled', - ); - } - }); + if (config.local.verification.send_email) { + promise = this.grpcSdk.config.get('email') + .then((emailConfig: any) => { + if (!emailConfig.active) { + throw ConduitError.forbidden('Cannot use email verification without Email module being enabled'); + } + }) + .catch(_ => { + throw ConduitError.forbidden('Cannot use email verification without Email module being enabled'); + }); } else { promise = Promise.resolve(); } @@ -53,6 +53,7 @@ export class LocalHandlers { return true; }) .catch((err: Error) => { + console.error(err.message); console.log('Local not active'); // De-initialize the provider if the config is now invalid this.initialized = false; @@ -65,10 +66,10 @@ export class LocalHandlers { throw new GrpcError(status.NOT_FOUND, 'Requested resource not found'); let { email, password } = call.request.params; - if (email.indexOf('+') !== -1) { + if (AuthUtils.invalidEmailAddress(email)) { throw new GrpcError( status.INVALID_ARGUMENT, - 'Email contains unsupported characters', + 'Invalid email address provided', ); } @@ -77,22 +78,21 @@ export class LocalHandlers { let user: User | null = await User.getInstance().findOne({ email }); if (!isNil(user)) throw new GrpcError(status.ALREADY_EXISTS, 'User already exists'); - let hashedPassword = await AuthUtils.hashPassword(password); - const isVerified = this.identifier === 'username'; + const hashedPassword = await AuthUtils.hashPassword(password); user = await User.getInstance().create({ email, hashedPassword, - isVerified, + isVerified: false, }); this.grpcSdk.bus?.publish('authentication:register:user', JSON.stringify(user)); const config = ConfigController.getInstance().config; - let serverConfig = await this.grpcSdk.config.getServerConfig(); - let url = serverConfig.url; + const serverConfig = await this.grpcSdk.config.getServerConfig(); + const url = serverConfig.url; - if (config.local.identifier === 'email' && config.local.sendVerificationEmail) { + if (config.local.verification.send_email) { let verificationToken: Token = await Token.getInstance().create({ type: TokenType.VERIFICATION_TOKEN, userId: user._id, @@ -123,10 +123,10 @@ export class LocalHandlers { const clientId = context.clientId; - if (email.indexOf('+') !== -1) { + if (AuthUtils.invalidEmailAddress(email)) { throw new GrpcError( status.INVALID_ARGUMENT, - 'Email contains unsupported characters', + 'Invalid email address provided', ); } @@ -149,7 +149,7 @@ export class LocalHandlers { throw new GrpcError(status.UNAUTHENTICATED, 'Invalid login credentials'); const config = ConfigController.getInstance().config; - if (config.local.verificationRequired && !user.isVerified) { + if (config.local.verification.required && !user.isVerified) { throw new GrpcError( status.PERMISSION_DENIED, 'You must verify your account to login', @@ -157,7 +157,7 @@ export class LocalHandlers { } if (user.hasTwoFA) { - const verificationSid = await AuthUtils.sendVerificationCode(this.sms,user.phoneNumber!); + const verificationSid = await AuthUtils.sendVerificationCode(this.smsModule, user.phoneNumber!); if (verificationSid === '') { throw new GrpcError(status.INTERNAL, 'Could not send verification code'); } @@ -218,7 +218,7 @@ export class LocalHandlers { } async forgotPassword(call: ParsedRouterRequest): Promise { - if (!this.initialized || isNil(this.emailModule)) { + if (!this.initialized) { throw new GrpcError(status.NOT_FOUND, 'Requested resource not found'); } @@ -227,7 +227,7 @@ export class LocalHandlers { const user: User | null = await User.getInstance().findOne({ email }); - if (isNil(user) || (config.local.verificationRequired && !user.isVerified)) + if (isNil(user) || (config.local.verification.required && !user.isVerified)) return 'Ok'; let oldToken: Token | null = await Token.getInstance().findOne({ @@ -255,7 +255,7 @@ export class LocalHandlers { } async resetPassword(call: ParsedRouterRequest): Promise { - if (!this.initialized || isNil(this.emailModule)) { + if (!this.initialized) { throw new GrpcError(status.NOT_FOUND, 'Requested resource not found'); } @@ -348,7 +348,7 @@ export class LocalHandlers { const hashedPassword = await AuthUtils.hashPassword(newPassword); if (dbUser.hasTwoFA) { - const verificationSid = await AuthUtils.sendVerificationCode(this.sms,dbUser.phoneNumber!); + const verificationSid = await AuthUtils.sendVerificationCode(this.smsModule, dbUser.phoneNumber!); if (verificationSid === '') { throw new GrpcError(status.INTERNAL, 'Could not send verification code'); } @@ -389,7 +389,7 @@ export class LocalHandlers { throw new GrpcError(status.UNAUTHENTICATED, 'Change password token not found'); } - const verified = await this.sms.verify(token.token, code); + const verified = await this.smsModule.verify(token.token, code); if (!verified.verified) { throw new GrpcError(status.UNAUTHENTICATED, 'Invalid code'); @@ -420,8 +420,8 @@ export class LocalHandlers { }); if (isNil(verificationTokenDoc)) { - if (config.local.verification_redirect_uri) { - return { redirect: config.local.verification_redirect_uri }; + if (config.local.verification.redirect_uri) { + return { redirect: config.verification.redirect_uri }; } else { return 'Email verified'; } @@ -443,8 +443,8 @@ export class LocalHandlers { this.grpcSdk.bus?.publish('authentication:verified:user', JSON.stringify(user)); - if (config.local.verification_redirect_uri) { - return { redirect: config.local.verification_redirect_uri }; + if (config.verification.redirect_uri) { + return { redirect: config.verification.redirect_uri }; } return 'Email verified'; } @@ -471,7 +471,7 @@ export class LocalHandlers { throw new GrpcError(status.UNAUTHENTICATED, 'Unauthorized'); } - const verificationSid = await AuthUtils.sendVerificationCode(this.sms,phoneNumber); + const verificationSid = await AuthUtils.sendVerificationCode(this.smsModule, phoneNumber); if (verificationSid === '') { throw new GrpcError(status.INTERNAL, 'Could not send verification code'); } @@ -513,7 +513,7 @@ export class LocalHandlers { 'No verification record for this user', ); - const verified = await this.sms.verify(verificationRecord.token, code); + const verified = await this.smsModule.verify(verificationRecord.token, code); if (!verified.verified) { throw new GrpcError(status.UNAUTHENTICATED, 'email and code do not match'); @@ -563,11 +563,9 @@ export class LocalHandlers { private async initDbAndEmail() { const config = ConfigController.getInstance().config; - if (config.local.identifier !== 'username') { + if (config.local.verification.send_email) { await this.grpcSdk.config.moduleExists('email'); - await this.grpcSdk.waitForExistence('email'); - this.emailModule = this.grpcSdk.emailProvider!; } @@ -578,7 +576,7 @@ export class LocalHandlers { if (config.twofa.enabled && !errorMessage) { // maybe check if verify is enabled in sms module await this.grpcSdk.waitForExistence('sms'); - this.sms = this.grpcSdk.sms!; + this.smsModule = this.grpcSdk.sms!; } else { console.log('sms 2fa not active'); } @@ -586,12 +584,12 @@ export class LocalHandlers { if ((config.phoneAuthentication.enabled) && !errorMessage) { // maybe check if verify is enabled in sms module await this.grpcSdk.waitForExistence('sms'); - this.sms = this.grpcSdk.sms!; + this.smsModule = this.grpcSdk.sms!; } else { console.log('phone authentication not active'); } - if (config.local.identifier === 'email') { + if (config.local.verification.send_email) { this.registerTemplates(); } this.initialized = true; @@ -613,5 +611,4 @@ export class LocalHandlers { console.error('Internal error while registering email templates'); }); } - } diff --git a/modules/authentication/src/migrations/localAuthConfig.migration.ts b/modules/authentication/src/migrations/localAuthConfig.migration.ts new file mode 100644 index 000000000..d42660bbb --- /dev/null +++ b/modules/authentication/src/migrations/localAuthConfig.migration.ts @@ -0,0 +1,30 @@ +import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; + +const legacyKeys = [ + 'sendVerificationEmail', + 'verificationRequired', + 'verification_redirect_uri', + 'identifier', +]; + +function configIsOutdated(authConfig: any) { + return Object.keys(authConfig.local).some(key => legacyKeys.includes(key)); +} + +export async function migrateLocalAuthConfig(grpcSdk: ConduitGrpcSdk) { + await grpcSdk.config.get('authentication') + .then(async (authConfig: any) => { + if (configIsOutdated(authConfig)) { + authConfig.local.verification = { + required: authConfig.local.verificationRequired, + send_email: authConfig.local.sendVerificationEmail, + redirect_uri: authConfig.local.verification_redirect_uri, + } + legacyKeys.forEach(key => { delete authConfig.local[key]; }); + await grpcSdk.config.updateConfig(authConfig, 'authentication'); + } + }) + .catch(err => { + console.log('nothing to update, no config') + }); +} diff --git a/modules/authentication/src/routes/routes.ts b/modules/authentication/src/routes/routes.ts index 1a93d338f..32a10374b 100644 --- a/modules/authentication/src/routes/routes.ts +++ b/modules/authentication/src/routes/routes.ts @@ -44,7 +44,7 @@ export class AuthenticationRoutes { private slackHandlers: SlackHandlers; private figmaHandlers: FigmaHandlers; private microsoftHandlers: MicrosoftHandlers; - private _routingManager: RoutingManager; + private readonly _routingManager: RoutingManager; constructor(readonly server: GrpcServer, private readonly grpcSdk: ConduitGrpcSdk) { this._routingManager = new RoutingManager(this.grpcSdk.router, server); @@ -79,16 +79,12 @@ export class AuthenticationRoutes { { path: '/local/new', action: ConduitRouteActions.POST, - description: `Creates a new user using either email/password or username/password. - The combination depends on the provided configuration. - In the case of email/password the email module is required and - the user will receive an email before being able to login.`, + description: 'Creates a new user using email/password.', bodyParams: { email: ConduitString.Required, password: ConduitString.Required, }, - middlewares: - authConfig.local.identifier === 'username' ? ['authMiddleware'] : [], + middlewares: [], }, new ConduitRouteReturnDefinition('RegisterResponse', { userId: ConduitString.Optional, @@ -115,7 +111,12 @@ export class AuthenticationRoutes { }), this.localHandlers.authenticate.bind(this.localHandlers), ); - if (authConfig.local.identifier !== 'username') { + + let emailOnline = false; + await this.grpcSdk.config.moduleExists('email') + .then(_ => { emailOnline = true; }) + .catch(_ => {}); + if (emailOnline) { this._routingManager.route( { path: '/forgot-password', @@ -142,6 +143,7 @@ export class AuthenticationRoutes { new ConduitRouteReturnDefinition('ResetPasswordResponse', 'String'), this.localHandlers.resetPassword.bind(this.localHandlers), ); + } this._routingManager.route( { @@ -159,20 +161,6 @@ export class AuthenticationRoutes { this.localHandlers.changePassword.bind(this.localHandlers), ); - this._routingManager.route( - { - path: '/local/change-password/verify', - action: ConduitRouteActions.POST, - description: `Used to provide the 2FA token for password change.`, - bodyParams: { - code: ConduitString.Required, - }, - middlewares: ['authMiddleware'], - }, - new ConduitRouteReturnDefinition('VerifyChangePasswordResponse', 'String'), - this.localHandlers.verifyChangePassword.bind(this.localHandlers), - ); - this._routingManager.route( { path: '/hook/verify-email/:verificationToken', @@ -186,7 +174,6 @@ export class AuthenticationRoutes { this.localHandlers.verifyEmail.bind(this.localHandlers), ); - } if (authConfig?.twofa.enabled) { this._routingManager.route( { @@ -246,6 +233,20 @@ export class AuthenticationRoutes { new ConduitRouteReturnDefinition('DisableTwoFaResponse', 'String'), this.localHandlers.disableTwoFa.bind(this.localHandlers), ); + + this._routingManager.route( + { + path: '/local/change-password/verify', + action: ConduitRouteActions.POST, + description: `Used to provide the 2FA token for password change.`, + bodyParams: { + code: ConduitString.Required, + }, + middlewares: ['authMiddleware'], + }, + new ConduitRouteReturnDefinition('VerifyChangePasswordResponse', 'String'), + this.localHandlers.verifyChangePassword.bind(this.localHandlers), + ); } enabled = true; } diff --git a/modules/authentication/src/utils/auth.ts b/modules/authentication/src/utils/auth.ts index e5434b5ac..e79acd478 100644 --- a/modules/authentication/src/utils/auth.ts +++ b/modules/authentication/src/utils/auth.ts @@ -156,4 +156,12 @@ export namespace AuthUtils { const verificationSid = await sms.sendVerificationCode(to); return verificationSid.verificationSid || ''; } + + export function invalidEmailAddress(email: string) { + return !email + .toLowerCase() + .match( + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + ); + } } diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json index 57fc2a7c9..af720e0df 100644 --- a/packages/config/tsconfig.json +++ b/packages/config/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5", + "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */