Skip to content

Commit 25a0900

Browse files
fix(auth): gate OAuth user creation behind config.sign.up (#3503) (#3507)
* fix(auth): gate OAuth user creation behind config.sign.up (#3503) `checkOAuthUserProfile` branch 4 (create new user) now returns a VALIDATION_ERROR when `config.sign.up` is false — matching the local signup endpoint's behavior. Branches 1-3 (primary lookup, linked identity, link-on-verified-email) remain open so existing users can always sign in, even during a signup freeze. The freeze only applies to fresh account creation via OAuth. Closes #3503 * fix(auth): align OAuth signup-disabled error with local signup + capture original config in test Address reviewer feedback on #3507: - Copilot (auth.controller.js): Use AppError message 'Signup error' to match the local signup endpoint (`signup` above), so clients see the same user-facing `message`/`description` regardless of signup method. - Copilot (auth.integration.tests.js): Capture `config.sign.up` in beforeEach and restore in afterEach instead of hard-coding `true`, so tests do not leak state when the original value is not `true` (consistent with the /api/auth/config suite pattern).
1 parent 247ba7b commit 25a0900

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

modules/auth/controllers/auth.controller.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,14 @@ const checkOAuthUserProfile = async (profil, key, provider) => {
372372
}
373373
// 4. No match → create new user
374374
try {
375+
if (!config.sign.up) {
376+
// Mirror the local signup endpoint's error shape so clients see the same
377+
// `message`/`description` regardless of signup method (see `signup` above).
378+
throw new AppError('Signup error', {
379+
code: 'VALIDATION_ERROR',
380+
details: { message: 'Registration is currently deactivated' },
381+
});
382+
}
375383
const user = {
376384
firstName: profil.firstName,
377385
lastName: profil.lastName,

modules/auth/tests/auth.integration.tests.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,127 @@ describe('Auth integration tests:', () => {
892892
).rejects.toMatchObject({ code: 'VALIDATION_ERROR' });
893893
});
894894

895+
describe('signup gate (config.sign.up = false)', () => {
896+
let originalSignUpConfig;
897+
898+
beforeEach(() => {
899+
originalSignUpConfig = config.sign.up;
900+
});
901+
902+
afterEach(() => {
903+
config.sign.up = originalSignUpConfig;
904+
});
905+
906+
test('should reject new OAuth user creation when signup is disabled (branch 4)', async () => {
907+
config.sign.up = false;
908+
const email = 'oauth-signup-gate-new@test.com';
909+
const profil = {
910+
firstName: 'Gated',
911+
lastName: 'Signup',
912+
email,
913+
avatar: '',
914+
providerData: { sub: 'google-gated-sub-3503' },
915+
emailVerifiedByProvider: false,
916+
};
917+
await expect(
918+
AuthController.checkOAuthUserProfile(profil, 'sub', 'google'),
919+
).rejects.toMatchObject({
920+
code: 'VALIDATION_ERROR',
921+
details: { message: 'Registration is currently deactivated' },
922+
});
923+
// Ensure no user was persisted
924+
const users = await UserService.search({ email });
925+
expect(users.length).toBe(0);
926+
});
927+
928+
test('should allow existing OAuth-first user to sign in when signup disabled (branch 1)', async () => {
929+
const email = 'oauth-signup-gate-b1@test.com';
930+
const existing = await UserService.create({
931+
firstName: 'Branch',
932+
lastName: 'One',
933+
email,
934+
provider: 'google',
935+
providerData: { sub: 'google-gated-b1-sub' },
936+
roles: ['user'],
937+
});
938+
config.sign.up = false;
939+
const profil = {
940+
firstName: 'Branch',
941+
lastName: 'One',
942+
email,
943+
avatar: '',
944+
providerData: { sub: 'google-gated-b1-sub' },
945+
};
946+
const found = await AuthController.checkOAuthUserProfile(profil, 'sub', 'google');
947+
expect(found).toBeDefined();
948+
expect(found.id).toBe(existing.id);
949+
950+
try { await UserService.remove(existing); } catch (_) { /* cleanup */ }
951+
});
952+
953+
test('should allow linked OAuth user to sign in when signup disabled (branch 2)', async () => {
954+
const email = 'oauth-signup-gate-b2@test.com';
955+
const localUser = await UserService.create({
956+
firstName: 'Branch',
957+
lastName: 'Two',
958+
email,
959+
password: 'W@os.jsI$Aw3$0m3',
960+
provider: 'local',
961+
roles: ['user'],
962+
});
963+
// Inject additionalProvidersData to simulate a pre-linked account
964+
const brutLocal = await UserService.getBrut({ email });
965+
await UserService.update(brutLocal, {
966+
additionalProvidersData: {
967+
google: { sub: 'google-gated-b2-sub', email_verified: true },
968+
},
969+
}, 'recover');
970+
971+
config.sign.up = false;
972+
const profil = {
973+
firstName: 'Branch',
974+
lastName: 'Two',
975+
email,
976+
avatar: '',
977+
providerData: { sub: 'google-gated-b2-sub' },
978+
};
979+
const found = await AuthController.checkOAuthUserProfile(profil, 'sub', 'google');
980+
expect(found).toBeDefined();
981+
expect(found.id).toBe(localUser.id);
982+
983+
try { await UserService.remove(localUser); } catch (_) { /* cleanup */ }
984+
});
985+
986+
test('should link local user via verified email when signup disabled (branch 3)', async () => {
987+
const email = 'oauth-signup-gate-b3@test.com';
988+
const localUser = await UserService.create({
989+
firstName: 'Branch',
990+
lastName: 'Three',
991+
email,
992+
password: 'W@os.jsI$Aw3$0m3',
993+
provider: 'local',
994+
roles: ['user'],
995+
});
996+
997+
config.sign.up = false;
998+
const profil = {
999+
firstName: 'Branch',
1000+
lastName: 'Three',
1001+
email,
1002+
avatar: '',
1003+
providerData: { sub: 'google-gated-b3-sub', email_verified: true },
1004+
emailVerifiedByProvider: true,
1005+
};
1006+
const linked = await AuthController.checkOAuthUserProfile(profil, 'sub', 'google');
1007+
expect(linked).toBeDefined();
1008+
expect(linked.id).toBe(localUser.id);
1009+
const brutLinked = await UserService.getBrut({ id: linked.id });
1010+
expect(brutLinked.additionalProvidersData?.google?.sub).toBe('google-gated-b3-sub');
1011+
1012+
try { await UserService.remove(linked); } catch (_) { /* cleanup */ }
1013+
});
1014+
});
1015+
8951016
test('token endpoint should strip accessToken/refreshToken from additionalProvidersData', async () => {
8961017
// Create a local user then sign in to get an auth cookie
8971018
const localEmail = 'token-sanitize@test.com';

0 commit comments

Comments
 (0)