Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
**/dist/
**/dist-test/
**/node_modules/
**/.nyc_output
/.idea
Expand Down
3 changes: 2 additions & 1 deletion modules/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"build": "rimraf dist && tsc",
"postbuild": "copyfiles -u 1 src/**/*.proto src/*.proto src/**/*.json ./dist/",
"build:docker": "docker build -t ghcr.io/conduitplatform/authentication:latest -f ./Dockerfile ../../ && docker push ghcr.io/conduitplatform/authentication:latest",
"generateTypes": "sh build.sh"
"generateTypes": "sh build.sh",
"test": "tsc -p tsconfig.test.json && copyfiles -u 1 src/data/*.json dist-test/ && node --test dist-test/utils/emailRestrictions.test.js"
},
"license": "ISC",
"directories": {
Expand Down
14 changes: 6 additions & 8 deletions modules/authentication/src/Authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ConduitGrpcSdk,
DatabaseProvider,
GrpcCallback,
GrpcError,
GrpcRequest,
HealthCheckStatus,
Indexable,
Expand Down Expand Up @@ -399,12 +400,7 @@ export default class Authentication extends ManagedModule<Config> {
if (user) {
return callback({ code: status.ALREADY_EXISTS, message: 'User already exists' });
}
if (AuthUtils.invalidEmailAddress(email)) {
return callback({
code: status.INVALID_ARGUMENT,
message: 'Invalid email address provided',
});
}
AuthUtils.assertValidEmail(email);
const hashedPassword = await AuthUtils.hashPassword(password);
const anonymousUserId = call.request.anonymousId;
if (!anonymousUserId) {
Expand Down Expand Up @@ -472,6 +468,9 @@ export default class Authentication extends ManagedModule<Config> {
}
return callback(null, { password });
} catch (e) {
if (e instanceof GrpcError) {
return callback({ code: e.code, message: e.message });
}
return callback({ code: status.INTERNAL, message: (e as Error).message });
}
}
Expand Down Expand Up @@ -614,8 +613,7 @@ export default class Authentication extends ManagedModule<Config> {
const request = createParsedRouterRequest(call.request);
try {
const team = (await new TeamsAdmin(this.grpcSdk).getTeam(request)) as
| models.Team
| undefined;
models.Team | undefined;
if (!team) {
return callback({ code: status.NOT_FOUND, message: 'Team not found' });
}
Expand Down
2 changes: 2 additions & 0 deletions modules/authentication/src/admin/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@conduitplatform/grpc-sdk';
import { status } from '@grpc/grpc-js';
import { AuthUtils } from '../utils/index.js';
import { assertEmailAllowed } from '../utils/emailRestrictions.js';
import { isNil } from 'lodash-es';
import { User } from '../models/index.js';
import escapeStringRegexp from 'escape-string-regexp';
Expand Down Expand Up @@ -91,6 +92,7 @@ export class UserAdmin {
if (!isNil(duplicateEmail)) {
throw new GrpcError(status.INVALID_ARGUMENT, 'Email already exists');
}
assertEmailAllowed(email);
}
let twoFaMethod: string | undefined;
if (hasTwoFA) {
Expand Down
51 changes: 51 additions & 0 deletions modules/authentication/src/config/emailRestrictions.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export default {
emailRestrictions: {
enabled: {
doc: 'Defines if email restrictions are enforced on new email intake',
format: 'Boolean',
default: false,
},
blockDisposableEmails: {
doc: 'Defines if emails from known disposable providers should be blocked',
format: 'Boolean',
default: true,
},
blockPlusAddressing: {
doc: 'Defines if plus addressing in the local part of an email should be blocked',
format: 'Boolean',
default: true,
},
blockedAddresses: {
doc: 'Exact email addresses that are not allowed',
format: 'Array',
children: {
format: 'String',
},
default: [],
},
blockedDomains: {
doc: 'Email domains that are not allowed, including subdomains',
format: 'Array',
children: {
format: 'String',
},
default: [],
},
allowedAddresses: {
doc: 'Exact email addresses that override block, disposable, and plus-addressing rules, but not reserved anonymous.com',
format: 'Array',
children: {
format: 'String',
},
default: [],
},
allowedDomains: {
doc: 'Email domains that override block, disposable, and plus-addressing rules, including subdomains, but not reserved anonymous.com',
format: 'Array',
children: {
format: 'String',
},
default: [],
},
},
};
2 changes: 2 additions & 0 deletions modules/authentication/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import appleConfig from './apple.config.js';
import twitterConfig from './twitter.config.js';
import teamsConfig from './teams.config.js';
import metamaskConfig from './metamask.config.js';
import emailRestrictionsConfig from './emailRestrictions.config.js';

const AppConfigSchema = {
...DefaultConfig,
...emailRestrictionsConfig,
...teamsConfig,
...figmaConfig,
...githubConfig,
Expand Down

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions modules/authentication/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ export const errors = {
message: 'Invalid invitation token',
description: 'The provided invitation token is invalid',
},
EMAIL_NOT_ALLOWED: {
conduitCode: 'EMAIL_NOT_ALLOWED',
grpcCode: status.INVALID_ARGUMENT,
message: 'This email address is not allowed',
description: 'The provided email address is blocked by email restrictions',
},
} as const satisfies Record<string, ModuleErrorDefinition>;
14 changes: 5 additions & 9 deletions modules/authentication/src/handlers/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class LocalHandlers implements IAuthenticationStrategy {
errors.USER_EXISTS,
errors.INVITATION_REQUIRED,
errors.INVALID_INVITATION,
errors.EMAIL_NOT_ALLOWED,
],
rateLimit: AUTH_CREDENTIALS,
},
Expand Down Expand Up @@ -188,6 +189,7 @@ export class LocalHandlers implements IAuthenticationStrategy {
redirectUri: ConduitString.Optional,
},
middlewares: ['authMiddleware', 'denyAnonymousMiddleware'],
errors: [errors.EMAIL_NOT_ALLOWED],
},
new ConduitRouteReturnDefinition('ChangeEmailResponse', 'String'),
this.changeEmail.bind(this),
Expand Down Expand Up @@ -311,10 +313,7 @@ export class LocalHandlers implements IAuthenticationStrategy {
return token?.data?.userData;
});
}
const invalidAddress = AuthUtils.invalidEmailAddress(email);
if (invalidAddress) {
throw new GrpcError(status.INVALID_ARGUMENT, 'Invalid email address provided');
}
AuthUtils.assertValidEmail(email, 'module');

let user: User | null = await User.getInstance().findOne({ email });
if (!isNil(user))
Expand Down Expand Up @@ -573,10 +572,7 @@ export class LocalHandlers implements IAuthenticationStrategy {
'The new email can not be the same as the old email',
);
}
const invalidAddress = AuthUtils.invalidEmailAddress(newEmail);
if (invalidAddress) {
throw new GrpcError(status.INVALID_ARGUMENT, 'Invalid email address provided');
}
AuthUtils.assertValidEmail(newEmail, 'module');
const dupEmailUser = await User.getInstance().findOne({ email: newEmail });
if (dupEmailUser) {
throw new GrpcError(status.ALREADY_EXISTS, 'Email address already taken');
Expand Down Expand Up @@ -666,7 +662,7 @@ export class LocalHandlers implements IAuthenticationStrategy {
}

async verifyChangeEmail(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
const { verificationToken } = call.request.params.verificationToken;
const verificationToken = call.request.params.verificationToken;
const config = ConfigController.getInstance().config;
const token: Token | null = await Token.getInstance().findOne(
{
Expand Down
4 changes: 4 additions & 0 deletions modules/authentication/src/handlers/oauth2/OAuth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
OAUTH_NATIVE_COMPLETE,
} from '../../constants/index.js';
import { AuthUtils } from '../../utils/index.js';
import { assertEmailAllowed } from '../../utils/emailRestrictions.js';

export abstract class OAuth2<
T,
Expand Down Expand Up @@ -302,6 +303,9 @@ export abstract class OAuth2<
if (!user.isVerified) user.isVerified = true;
user = await User.getInstance().findByIdAndUpdate(user._id, user);
} else {
if (payload.email) {
assertEmailAllowed(payload.email);
}
if (anonymousUser) {
return (await User.getInstance().findByIdAndUpdate(anonymousUser._id, {
email: payload.email,
Expand Down
4 changes: 4 additions & 0 deletions modules/authentication/src/handlers/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Team as TeamAuthz } from '../authz/index.js';
import { TeamInviteTemplate } from '../templates/index.js';
import { status } from '@grpc/grpc-js';
import { AuthUtils } from '../utils/index.js';
import { assertEmailAllowed } from '../utils/emailRestrictions.js';
import { IAuthenticationStrategy } from '../interfaces/index.js';
import { OAUTH_CALLBACK, TokenType } from '../constants/index.js';
import { v4 as uuid } from 'uuid';
Expand Down Expand Up @@ -589,6 +590,9 @@ export class TeamsHandler implements IAuthenticationStrategy {
'You do not have permission to invite users to this team',
);
}
if (email) {
assertEmailAllowed(email);
}

// Delete any existing invite for the same email and team
await Token.getInstance().deleteOne({
Expand Down
Loading