Skip to content
Open
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
2 changes: 1 addition & 1 deletion modules/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"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",
"test": "tsc -p tsconfig.test.json && copyfiles -u 1 src/data/*.json dist-test/ && node --test dist-test/utils/emailRestrictions.test.js"
"test": "tsc -p tsconfig.test.json && copyfiles -u 1 src/data/*.json dist-test/ && node --test dist-test/utils/emailRestrictions.test.js dist-test/handlers/oauth2/utils/oauthMode.test.js"
},
"license": "ISC",
"directories": {
Expand Down
6 changes: 6 additions & 0 deletions modules/authentication/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@ export const errors = {
message: 'This email address is not allowed',
description: 'The provided email address is blocked by email restrictions',
},
REGISTRATION_NOT_ALLOWED: {
conduitCode: 'REGISTRATION_NOT_ALLOWED',
grpcCode: status.PERMISSION_DENIED,
message: 'User registration is not allowed for this request',
description: 'OAuth was started in sign-in-only mode and no existing user was found',
},
} as const satisfies Record<string, ModuleErrorDefinition>;
24 changes: 21 additions & 3 deletions modules/authentication/src/handlers/oauth2/OAuth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ import { TokenProvider } from '../tokenProvider.js';
import { v4 as uuid } from 'uuid';
import { createHash } from 'crypto';
import { TeamsHandler } from '../team.js';
import { validateStateToken } from './utils/index.js';
import {
assertOAuthRegistrationAllowed,
OAUTH_MODE_PARAM,
resolveOAuthMode,
type OAuthMode,
validateStateToken,
} from './utils/index.js';
import { errors } from '../../errors.js';
import { IAuthenticationStrategy } from '../../interfaces/index.js';
import { TokenType } from '../../constants/index.js';
import {
Expand Down Expand Up @@ -91,6 +98,7 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
expiresAt: new Date(Date.now() + 10 * 60 * 1000),
customRedirectUri: call.request.params.redirectUri,
anonymousUserId: anonymousUser?._id,
mode: resolveOAuthMode(call.request.params?.mode),
},
})
.catch(err => {
Expand Down Expand Up @@ -144,6 +152,7 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
expiresAt: new Date(Date.now() + 10 * 60 * 1000),
customRedirectUri: call.request.params.redirectUri,
anonymousUserId: anonymousUser?._id,
mode: resolveOAuthMode(call.request.params?.mode),
},
})
.catch(err => {
Expand Down Expand Up @@ -204,6 +213,7 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
payload,
stateToken.data.invitationToken,
stateToken.data.anonymousUserId,
resolveOAuthMode(stateToken.data.mode),
);
ConduitGrpcSdk.Metrics?.increment('logged_in_users_total');

Expand Down Expand Up @@ -234,6 +244,7 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
payload,
call.request.params.invitationToken,
call.request.context.anonymousUser?._id,
resolveOAuthMode(call.request.params?.mode),
);
const config = ConfigController.getInstance().config;
ConduitGrpcSdk.Metrics?.increment('logged_in_users_total');
Expand All @@ -249,6 +260,7 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
payload: Payload<T>,
invitationToken?: string,
anonymousUserId?: string,
mode: OAuthMode = 'both',
): Promise<User> {
let user: User | null = null;
if (payload.hasOwnProperty('email') && !isNil(payload.email)) {
Expand Down Expand Up @@ -299,6 +311,7 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
if (!user.isVerified) user.isVerified = true;
user = await User.getInstance().findByIdAndUpdate(user._id, user);
} else {
assertOAuthRegistrationAllowed(mode);
if (payload.email) {
assertEmailAllowed(payload.email);
}
Expand Down Expand Up @@ -358,13 +371,14 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
routingManager.route(
{
path: `/init/${this.providerName}`,
description: `Begins ${this.capitalizeProvider()} authentication.`,
description: `Begins ${this.capitalizeProvider()} authentication. Optional mode: "both" (default, login and register) or "signIn" (existing users only).`,
action: ConduitRouteActions.GET,
queryParams: {
scopes: [ConduitString.Optional],
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
redirectUri: ConduitString.Optional,
mode: OAUTH_MODE_PARAM,
},
middlewares: initRouteMiddleware,
},
Expand All @@ -378,12 +392,13 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
routingManager.route(
{
path: `/initNative/${this.providerName}`,
description: `Begins ${this.capitalizeProvider()} native authentication.`,
description: `Begins ${this.capitalizeProvider()} native authentication. Optional mode: "both" (default, login and register) or "signIn" (existing users only).`,
action: ConduitRouteActions.GET,
queryParams: {
scopes: [ConduitString.Optional],
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
mode: OAUTH_MODE_PARAM,
},
middlewares: initRouteMiddleware,
},
Expand All @@ -403,6 +418,7 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
id_token: ConduitString.Required,
state: ConduitString.Required,
},
errors: [errors.REGISTRATION_NOT_ALLOWED],
},
new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, {
accessToken: ConduitString.Optional,
Expand All @@ -422,6 +438,7 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
code: ConduitString.Required,
state: ConduitString.Required,
},
errors: [errors.REGISTRATION_NOT_ALLOWED],
},
new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, {
accessToken: ConduitString.Optional,
Expand All @@ -439,6 +456,7 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
code: ConduitString.Required,
state: ConduitString.Required,
},
errors: [errors.REGISTRATION_NOT_ALLOWED],
},
new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, {
accessToken: ConduitString.Optional,
Expand Down
6 changes: 5 additions & 1 deletion modules/authentication/src/handlers/oauth2/apple/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ import moment from 'moment';
import jwksRsa from 'jwks-rsa';
import qs from 'querystring';

import { validateStateToken } from '../utils/index.js';
import { validateStateToken, resolveOAuthMode } from '../utils/index.js';
import {
ConduitString,
ConfigController,
RoutingManager,
} from '@conduitplatform/module-tools';
import { AuthUtils } from '../../../utils/index.js';
import { errors } from '../../../errors.js';

export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
constructor(grpcSdk: ConduitGrpcSdk, config: { apple: AppleProviderConfig }) {
Expand Down Expand Up @@ -142,6 +143,7 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
userParams,
stateToken.data.invitationToken,
stateToken.data.anonymousUserId,
resolveOAuthMode(stateToken.data.mode),
);
await Token.getInstance().deleteOne(stateToken);
ConduitGrpcSdk.Metrics?.increment('logged_in_users_total');
Expand Down Expand Up @@ -236,6 +238,7 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
userParams,
stateToken.data.invitationToken,
stateToken.data.anonymousUserId,
resolveOAuthMode(stateToken.data.mode),
);
await Token.getInstance().deleteOne(stateToken);
ConduitGrpcSdk.Metrics?.increment('logged_in_users_total');
Expand All @@ -261,6 +264,7 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
id_token: ConduitString.Required,
state: ConduitString.Required,
},
errors: [errors.REGISTRATION_NOT_ALLOWED],
},
new ConduitRouteReturnDefinition(`AppleResponse`, {
accessToken: ConduitString.Optional,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
} from '../interfaces/index.js';
import { OAuth2 } from '../OAuth2.js';
import { FacebookUser } from './facebook.user.js';
import { OAUTH_MODE_PARAM } from '../utils/index.js';
import { errors } from '../../../errors.js';

// todo migrate to use native method properly
export class FacebookHandlers extends OAuth2<FacebookUser, OAuth2Settings> {
Expand Down Expand Up @@ -79,14 +81,16 @@ export class FacebookHandlers extends OAuth2<FacebookUser, OAuth2Settings> {
{
path: '/facebook',
action: ConduitRouteActions.POST,
description: `Login/register with Facebook by providing a token from the client.`,
description: `Login/register with Facebook by providing a token from the client. Optional mode: "both" (default, login and register) or "signIn" (existing users only).`,
bodyParams: {
access_token: ConduitString.Required,
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
scopes: [ConduitString.Optional],
mode: OAUTH_MODE_PARAM,
},
middlewares: ['authMiddleware?', 'checkAnonymousMiddleware'],
errors: [errors.REGISTRATION_NOT_ALLOWED],
},
new ConduitRouteReturnDefinition('FacebookResponse', {
userId: ConduitString.Required,
Expand Down
6 changes: 5 additions & 1 deletion modules/authentication/src/handlers/oauth2/google/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
Payload,
ProviderConfig,
} from '../interfaces/index.js';
import { OAUTH_MODE_PARAM } from '../utils/index.js';
import { errors } from '../../../errors.js';

// todo migrate to use native method properly
export class GoogleHandlers extends OAuth2<GoogleUser, OAuth2Settings> {
Expand Down Expand Up @@ -61,16 +63,18 @@ export class GoogleHandlers extends OAuth2<GoogleUser, OAuth2Settings> {
{
path: '/google',
action: ConduitRouteActions.POST,
description: `Login/register with Google by providing a token from the client.`,
description: `Login/register with Google by providing a token from the client. Optional mode: "both" (default, login and register) or "signIn" (existing users only).`,
bodyParams: {
id_token: ConduitString.Required,
access_token: ConduitString.Required,
expires_in: ConduitString.Optional,
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
scopes: [ConduitString.Optional],
mode: OAUTH_MODE_PARAM,
},
middlewares: ['authMiddleware?', 'checkAnonymousMiddleware'],
errors: [errors.REGISTRATION_NOT_ALLOWED],
},
new ConduitRouteReturnDefinition('GoogleResponse', {
userId: ConduitString.Required,
Expand Down
1 change: 1 addition & 0 deletions modules/authentication/src/handlers/oauth2/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './ValidateStateToken.js';
export * from './MakeRequest.js';
export * from './oauthMode.js';
58 changes: 58 additions & 0 deletions modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { GrpcError } from '@conduitplatform/grpc-sdk';
import { ModuleError } from '@conduitplatform/module-tools';
import { status } from '@grpc/grpc-js';
import { errors } from '../../../errors.js';
import { assertOAuthRegistrationAllowed, resolveOAuthMode } from './oauthMode.js';

describe('resolveOAuthMode', () => {
it('defaults to both when the value is omitted', () => {
assert.equal(resolveOAuthMode(undefined), 'both');
assert.equal(resolveOAuthMode(null), 'both');
assert.equal(resolveOAuthMode(''), 'both');
});

it('returns both when both is provided', () => {
assert.equal(resolveOAuthMode('both'), 'both');
});

it('returns signIn when signIn is provided', () => {
assert.equal(resolveOAuthMode('signIn'), 'signIn');
});

it('throws INVALID_ARGUMENT for an invalid value', () => {
assert.throws(
() => resolveOAuthMode('register'),
(err: unknown) =>
err instanceof GrpcError &&
err.code === status.INVALID_ARGUMENT &&
err.message.includes('mode must be "signIn" or "both"'),
);
});
});

describe('assertOAuthRegistrationAllowed', () => {
it('allows registration when mode is both', () => {
assert.doesNotThrow(() => assertOAuthRegistrationAllowed('both'));
});

it('throws REGISTRATION_NOT_ALLOWED when mode is signIn', () => {
assert.throws(
() => assertOAuthRegistrationAllowed('signIn'),
(err: unknown) => {
if (!(err instanceof ModuleError) || err.code !== status.PERMISSION_DENIED) {
return false;
}
const parsed = JSON.parse(err.message) as {
conduitCode: string;
message: string;
};
return (
parsed.conduitCode === errors.REGISTRATION_NOT_ALLOWED.conduitCode &&
parsed.message === errors.REGISTRATION_NOT_ALLOWED.message
);
},
);
});
});
36 changes: 36 additions & 0 deletions modules/authentication/src/handlers/oauth2/utils/oauthMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { GrpcError } from '@conduitplatform/grpc-sdk';
import { ConduitString, ModuleError } from '@conduitplatform/module-tools';
import { status } from '@grpc/grpc-js';
import { errors } from '../../../errors.js';

export const OAUTH_MODES = ['signIn', 'both'] as const;
export type OAuthMode = (typeof OAUTH_MODES)[number];

export const OAUTH_MODE_PARAM = ConduitString.Optional;

export function isOAuthMode(value: unknown): value is OAuthMode {
return value === 'signIn' || value === 'both';
}

export function resolveOAuthMode(value: unknown): OAuthMode {
if (value === undefined || value === null || value === '') {
return 'both';
}
if (isOAuthMode(value)) {
return value;
}
throw new GrpcError(status.INVALID_ARGUMENT, 'mode must be "signIn" or "both"');
}

export function assertOAuthRegistrationAllowed(mode: OAuthMode): void {
switch (mode) {
case 'both':
return;
case 'signIn':
throw new ModuleError(errors.REGISTRATION_NOT_ALLOWED);
default: {
const _exhaustive: never = mode;
throw new GrpcError(status.INTERNAL, `Unhandled OAuth mode: ${_exhaustive}`);
}
}
}
2 changes: 2 additions & 0 deletions modules/authentication/tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"include": [
"src/utils/emailRestrictions.ts",
"src/utils/emailRestrictions.test.ts",
"src/handlers/oauth2/utils/oauthMode.ts",
"src/handlers/oauth2/utils/oauthMode.test.ts",
"src/errors.ts",
"src/data/disposable-email-domains.json"
],
Expand Down