From 88549eb1421a56713160354d540573f8d2973b8b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 10 Sep 2026 09:41:44 +0000 Subject: [PATCH 1/3] feat(authentication): add OAuth sign-in-only mode Allow OAuth init and token routes to request signIn or both (default). Sign-in-only blocks new user creation so clients can collect terms and profile data before registration. --- modules/authentication/package.json | 2 +- modules/authentication/src/errors.ts | 6 ++ .../src/handlers/oauth2/OAuth2.ts | 24 +++++++- .../src/handlers/oauth2/apple/apple.ts | 6 +- .../src/handlers/oauth2/facebook/facebook.ts | 6 +- .../src/handlers/oauth2/google/google.ts | 6 +- .../src/handlers/oauth2/utils/index.ts | 1 + .../handlers/oauth2/utils/oauthMode.test.ts | 58 +++++++++++++++++++ .../src/handlers/oauth2/utils/oauthMode.ts | 36 ++++++++++++ modules/authentication/tsconfig.test.json | 2 + 10 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts create mode 100644 modules/authentication/src/handlers/oauth2/utils/oauthMode.ts diff --git a/modules/authentication/package.json b/modules/authentication/package.json index 6dec4f51d..f69b3b8aa 100644 --- a/modules/authentication/package.json +++ b/modules/authentication/package.json @@ -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": { diff --git a/modules/authentication/src/errors.ts b/modules/authentication/src/errors.ts index cdd0111d8..2939efe84 100644 --- a/modules/authentication/src/errors.ts +++ b/modules/authentication/src/errors.ts @@ -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; diff --git a/modules/authentication/src/handlers/oauth2/OAuth2.ts b/modules/authentication/src/handlers/oauth2/OAuth2.ts index fdb28c693..827d179e4 100644 --- a/modules/authentication/src/handlers/oauth2/OAuth2.ts +++ b/modules/authentication/src/handlers/oauth2/OAuth2.ts @@ -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 { @@ -91,6 +98,7 @@ export abstract class OAuth2 expiresAt: new Date(Date.now() + 10 * 60 * 1000), customRedirectUri: call.request.params.redirectUri, anonymousUserId: anonymousUser?._id, + mode: resolveOAuthMode(call.request.params?.mode), }, }) .catch(err => { @@ -144,6 +152,7 @@ export abstract class OAuth2 expiresAt: new Date(Date.now() + 10 * 60 * 1000), customRedirectUri: call.request.params.redirectUri, anonymousUserId: anonymousUser?._id, + mode: resolveOAuthMode(call.request.params?.mode), }, }) .catch(err => { @@ -204,6 +213,7 @@ export abstract class OAuth2 payload, stateToken.data.invitationToken, stateToken.data.anonymousUserId, + resolveOAuthMode(stateToken.data.mode), ); ConduitGrpcSdk.Metrics?.increment('logged_in_users_total'); @@ -234,6 +244,7 @@ export abstract class OAuth2 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'); @@ -249,6 +260,7 @@ export abstract class OAuth2 payload: Payload, invitationToken?: string, anonymousUserId?: string, + mode: OAuthMode = 'both', ): Promise { let user: User | null = null; if (payload.hasOwnProperty('email') && !isNil(payload.email)) { @@ -299,6 +311,7 @@ export abstract class OAuth2 if (!user.isVerified) user.isVerified = true; user = await User.getInstance().findByIdAndUpdate(user._id, user); } else { + assertOAuthRegistrationAllowed(mode); if (payload.email) { assertEmailAllowed(payload.email); } @@ -358,13 +371,14 @@ export abstract class OAuth2 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, }, @@ -378,12 +392,13 @@ export abstract class OAuth2 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, }, @@ -403,6 +418,7 @@ export abstract class OAuth2 id_token: ConduitString.Required, state: ConduitString.Required, }, + errors: [errors.REGISTRATION_NOT_ALLOWED], }, new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, { accessToken: ConduitString.Optional, @@ -422,6 +438,7 @@ export abstract class OAuth2 code: ConduitString.Required, state: ConduitString.Required, }, + errors: [errors.REGISTRATION_NOT_ALLOWED], }, new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, { accessToken: ConduitString.Optional, @@ -439,6 +456,7 @@ export abstract class OAuth2 code: ConduitString.Required, state: ConduitString.Required, }, + errors: [errors.REGISTRATION_NOT_ALLOWED], }, new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, { accessToken: ConduitString.Optional, diff --git a/modules/authentication/src/handlers/oauth2/apple/apple.ts b/modules/authentication/src/handlers/oauth2/apple/apple.ts index a98d5f096..7404f83aa 100644 --- a/modules/authentication/src/handlers/oauth2/apple/apple.ts +++ b/modules/authentication/src/handlers/oauth2/apple/apple.ts @@ -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 { constructor(grpcSdk: ConduitGrpcSdk, config: { apple: AppleProviderConfig }) { @@ -142,6 +143,7 @@ export class AppleHandlers extends OAuth2 { userParams, stateToken.data.invitationToken, stateToken.data.anonymousUserId, + resolveOAuthMode(stateToken.data.mode), ); await Token.getInstance().deleteOne(stateToken); ConduitGrpcSdk.Metrics?.increment('logged_in_users_total'); @@ -236,6 +238,7 @@ export class AppleHandlers extends OAuth2 { userParams, stateToken.data.invitationToken, stateToken.data.anonymousUserId, + resolveOAuthMode(stateToken.data.mode), ); await Token.getInstance().deleteOne(stateToken); ConduitGrpcSdk.Metrics?.increment('logged_in_users_total'); @@ -261,6 +264,7 @@ export class AppleHandlers extends OAuth2 { id_token: ConduitString.Required, state: ConduitString.Required, }, + errors: [errors.REGISTRATION_NOT_ALLOWED], }, new ConduitRouteReturnDefinition(`AppleResponse`, { accessToken: ConduitString.Optional, diff --git a/modules/authentication/src/handlers/oauth2/facebook/facebook.ts b/modules/authentication/src/handlers/oauth2/facebook/facebook.ts index 7d9b2ed2a..228397560 100644 --- a/modules/authentication/src/handlers/oauth2/facebook/facebook.ts +++ b/modules/authentication/src/handlers/oauth2/facebook/facebook.ts @@ -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 { @@ -79,14 +81,16 @@ export class FacebookHandlers extends OAuth2 { { 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, diff --git a/modules/authentication/src/handlers/oauth2/google/google.ts b/modules/authentication/src/handlers/oauth2/google/google.ts index 4fb85d6e3..589ea5f50 100644 --- a/modules/authentication/src/handlers/oauth2/google/google.ts +++ b/modules/authentication/src/handlers/oauth2/google/google.ts @@ -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 { @@ -61,7 +63,7 @@ export class GoogleHandlers extends OAuth2 { { 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, @@ -69,8 +71,10 @@ export class GoogleHandlers extends OAuth2 { 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, diff --git a/modules/authentication/src/handlers/oauth2/utils/index.ts b/modules/authentication/src/handlers/oauth2/utils/index.ts index 0bfd51d50..3d3b76c9c 100644 --- a/modules/authentication/src/handlers/oauth2/utils/index.ts +++ b/modules/authentication/src/handlers/oauth2/utils/index.ts @@ -1,2 +1,3 @@ export * from './ValidateStateToken.js'; export * from './MakeRequest.js'; +export * from './oauthMode.js'; diff --git a/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts b/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts new file mode 100644 index 000000000..1cc67bdb8 --- /dev/null +++ b/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts @@ -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 + ); + }, + ); + }); +}); diff --git a/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts b/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts new file mode 100644 index 000000000..c71650326 --- /dev/null +++ b/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts @@ -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}`); + } + } +} diff --git a/modules/authentication/tsconfig.test.json b/modules/authentication/tsconfig.test.json index d928ee996..74e69b6c2 100644 --- a/modules/authentication/tsconfig.test.json +++ b/modules/authentication/tsconfig.test.json @@ -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" ], From a1144945ec834b8855a9eed7b9153a9b672eb344 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 10 Sep 2026 10:48:02 +0000 Subject: [PATCH 2/3] feat(authentication): cap OAuth mode with config Request mode remains a per-call UX switch. Provider allowRegistration (default true) is the policy ceiling so clients cannot force account creation when registration is disabled. --- .../src/constants/Oauth2.default.schema.ts | 7 ++++++ modules/authentication/src/errors.ts | 3 ++- .../src/handlers/oauth2/OAuth2.ts | 4 ++++ .../oauth2/interfaces/ProviderConfig.ts | 1 + .../handlers/oauth2/utils/oauthMode.test.ts | 13 ++++++++++ .../src/handlers/oauth2/utils/oauthMode.ts | 24 +++++++++---------- 6 files changed, 39 insertions(+), 13 deletions(-) diff --git a/modules/authentication/src/constants/Oauth2.default.schema.ts b/modules/authentication/src/constants/Oauth2.default.schema.ts index 484cb5c45..22677ef7d 100644 --- a/modules/authentication/src/constants/Oauth2.default.schema.ts +++ b/modules/authentication/src/constants/Oauth2.default.schema.ts @@ -25,4 +25,11 @@ export const oauth2Schema = { format: 'Boolean', default: true, }, + allowRegistration: { + doc: + 'When disabled, this provider can only sign in existing users. ' + + 'Request mode=both cannot override this.', + format: 'Boolean', + default: true, + }, }; diff --git a/modules/authentication/src/errors.ts b/modules/authentication/src/errors.ts index 2939efe84..44e342a66 100644 --- a/modules/authentication/src/errors.ts +++ b/modules/authentication/src/errors.ts @@ -30,6 +30,7 @@ export const errors = { 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', + description: + 'OAuth registration is disabled for this request (sign-in-only mode or provider allowRegistration is false)', }, } as const satisfies Record; diff --git a/modules/authentication/src/handlers/oauth2/OAuth2.ts b/modules/authentication/src/handlers/oauth2/OAuth2.ts index 827d179e4..0a0e3a6ef 100644 --- a/modules/authentication/src/handlers/oauth2/OAuth2.ts +++ b/modules/authentication/src/handlers/oauth2/OAuth2.ts @@ -262,6 +262,10 @@ export abstract class OAuth2 anonymousUserId?: string, mode: OAuthMode = 'both', ): Promise { + const allowRegistration = + ConfigController.getInstance().config[this.providerName]?.allowRegistration !== + false; + mode = resolveOAuthMode(mode, allowRegistration); let user: User | null = null; if (payload.hasOwnProperty('email') && !isNil(payload.email)) { user = await User.getInstance().findOne({ diff --git a/modules/authentication/src/handlers/oauth2/interfaces/ProviderConfig.ts b/modules/authentication/src/handlers/oauth2/interfaces/ProviderConfig.ts index cfb2323bf..8e450a3a2 100644 --- a/modules/authentication/src/handlers/oauth2/interfaces/ProviderConfig.ts +++ b/modules/authentication/src/handlers/oauth2/interfaces/ProviderConfig.ts @@ -1,5 +1,6 @@ export interface ProviderConfig { accountLinking: boolean; + allowRegistration?: boolean; clientId: string; clientSecret?: string; redirect_uri: string; diff --git a/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts b/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts index 1cc67bdb8..a3c5bd7a3 100644 --- a/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts +++ b/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts @@ -30,6 +30,19 @@ describe('resolveOAuthMode', () => { err.message.includes('mode must be "signIn" or "both"'), ); }); + + it('caps to signIn when provider registration is disabled', () => { + assert.equal(resolveOAuthMode(undefined, false), 'signIn'); + assert.equal(resolveOAuthMode('both', false), 'signIn'); + assert.equal(resolveOAuthMode('signIn', false), 'signIn'); + }); + + it('still rejects invalid values when registration is disabled', () => { + assert.throws( + () => resolveOAuthMode('register', false), + (err: unknown) => err instanceof GrpcError && err.code === status.INVALID_ARGUMENT, + ); + }); }); describe('assertOAuthRegistrationAllowed', () => { diff --git a/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts b/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts index c71650326..57a91106b 100644 --- a/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts +++ b/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts @@ -3,23 +3,23 @@ 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 type OAuthMode = 'signIn' | 'both'; 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 { +export function resolveOAuthMode( + value: unknown, + allowRegistration: boolean = true, +): OAuthMode { + let requested: OAuthMode; if (value === undefined || value === null || value === '') { - return 'both'; - } - if (isOAuthMode(value)) { - return value; + requested = 'both'; + } else if (value === 'signIn' || value === 'both') { + requested = value; + } else { + throw new GrpcError(status.INVALID_ARGUMENT, 'mode must be "signIn" or "both"'); } - throw new GrpcError(status.INVALID_ARGUMENT, 'mode must be "signIn" or "both"'); + return allowRegistration ? requested : 'signIn'; } export function assertOAuthRegistrationAllowed(mode: OAuthMode): void { From af91f5cc0e7f2ff063d7299b7422e20e2c8e12aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 10 Sep 2026 10:49:51 +0000 Subject: [PATCH 3/3] Revert "feat(authentication): cap OAuth mode with config" This reverts commit a1144945ec834b8855a9eed7b9153a9b672eb344. --- .../src/constants/Oauth2.default.schema.ts | 7 ------ modules/authentication/src/errors.ts | 3 +-- .../src/handlers/oauth2/OAuth2.ts | 4 ---- .../oauth2/interfaces/ProviderConfig.ts | 1 - .../handlers/oauth2/utils/oauthMode.test.ts | 13 ---------- .../src/handlers/oauth2/utils/oauthMode.ts | 24 +++++++++---------- 6 files changed, 13 insertions(+), 39 deletions(-) diff --git a/modules/authentication/src/constants/Oauth2.default.schema.ts b/modules/authentication/src/constants/Oauth2.default.schema.ts index 22677ef7d..484cb5c45 100644 --- a/modules/authentication/src/constants/Oauth2.default.schema.ts +++ b/modules/authentication/src/constants/Oauth2.default.schema.ts @@ -25,11 +25,4 @@ export const oauth2Schema = { format: 'Boolean', default: true, }, - allowRegistration: { - doc: - 'When disabled, this provider can only sign in existing users. ' + - 'Request mode=both cannot override this.', - format: 'Boolean', - default: true, - }, }; diff --git a/modules/authentication/src/errors.ts b/modules/authentication/src/errors.ts index 44e342a66..2939efe84 100644 --- a/modules/authentication/src/errors.ts +++ b/modules/authentication/src/errors.ts @@ -30,7 +30,6 @@ export const errors = { conduitCode: 'REGISTRATION_NOT_ALLOWED', grpcCode: status.PERMISSION_DENIED, message: 'User registration is not allowed for this request', - description: - 'OAuth registration is disabled for this request (sign-in-only mode or provider allowRegistration is false)', + description: 'OAuth was started in sign-in-only mode and no existing user was found', }, } as const satisfies Record; diff --git a/modules/authentication/src/handlers/oauth2/OAuth2.ts b/modules/authentication/src/handlers/oauth2/OAuth2.ts index 0a0e3a6ef..827d179e4 100644 --- a/modules/authentication/src/handlers/oauth2/OAuth2.ts +++ b/modules/authentication/src/handlers/oauth2/OAuth2.ts @@ -262,10 +262,6 @@ export abstract class OAuth2 anonymousUserId?: string, mode: OAuthMode = 'both', ): Promise { - const allowRegistration = - ConfigController.getInstance().config[this.providerName]?.allowRegistration !== - false; - mode = resolveOAuthMode(mode, allowRegistration); let user: User | null = null; if (payload.hasOwnProperty('email') && !isNil(payload.email)) { user = await User.getInstance().findOne({ diff --git a/modules/authentication/src/handlers/oauth2/interfaces/ProviderConfig.ts b/modules/authentication/src/handlers/oauth2/interfaces/ProviderConfig.ts index 8e450a3a2..cfb2323bf 100644 --- a/modules/authentication/src/handlers/oauth2/interfaces/ProviderConfig.ts +++ b/modules/authentication/src/handlers/oauth2/interfaces/ProviderConfig.ts @@ -1,6 +1,5 @@ export interface ProviderConfig { accountLinking: boolean; - allowRegistration?: boolean; clientId: string; clientSecret?: string; redirect_uri: string; diff --git a/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts b/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts index a3c5bd7a3..1cc67bdb8 100644 --- a/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts +++ b/modules/authentication/src/handlers/oauth2/utils/oauthMode.test.ts @@ -30,19 +30,6 @@ describe('resolveOAuthMode', () => { err.message.includes('mode must be "signIn" or "both"'), ); }); - - it('caps to signIn when provider registration is disabled', () => { - assert.equal(resolveOAuthMode(undefined, false), 'signIn'); - assert.equal(resolveOAuthMode('both', false), 'signIn'); - assert.equal(resolveOAuthMode('signIn', false), 'signIn'); - }); - - it('still rejects invalid values when registration is disabled', () => { - assert.throws( - () => resolveOAuthMode('register', false), - (err: unknown) => err instanceof GrpcError && err.code === status.INVALID_ARGUMENT, - ); - }); }); describe('assertOAuthRegistrationAllowed', () => { diff --git a/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts b/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts index 57a91106b..c71650326 100644 --- a/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts +++ b/modules/authentication/src/handlers/oauth2/utils/oauthMode.ts @@ -3,23 +3,23 @@ import { ConduitString, ModuleError } from '@conduitplatform/module-tools'; import { status } from '@grpc/grpc-js'; import { errors } from '../../../errors.js'; -export type OAuthMode = 'signIn' | 'both'; +export const OAUTH_MODES = ['signIn', 'both'] as const; +export type OAuthMode = (typeof OAUTH_MODES)[number]; export const OAUTH_MODE_PARAM = ConduitString.Optional; -export function resolveOAuthMode( - value: unknown, - allowRegistration: boolean = true, -): OAuthMode { - let requested: OAuthMode; +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 === '') { - requested = 'both'; - } else if (value === 'signIn' || value === 'both') { - requested = value; - } else { - throw new GrpcError(status.INVALID_ARGUMENT, 'mode must be "signIn" or "both"'); + return 'both'; + } + if (isOAuthMode(value)) { + return value; } - return allowRegistration ? requested : 'signIn'; + throw new GrpcError(status.INVALID_ARGUMENT, 'mode must be "signIn" or "both"'); } export function assertOAuthRegistrationAllowed(mode: OAuthMode): void {