From 5040ae2c103152c783c49217a47d9f923dd6462e Mon Sep 17 00:00:00 2001 From: Hampus Date: Fri, 2 Oct 2026 17:23:03 +0200 Subject: [PATCH] fix(sso): join provisioned users to the single community (#3118) --- fluxer_api/src/api/auth/AuthController.ts | 4 ++- fluxer_api/src/api/auth/AuthRequestService.ts | 6 +++-- .../src/api/auth/services/SsoService.ts | 24 +++++++++++++++--- fluxer_api/src/api/auth/tests/SsoFlow.test.ts | 25 +++++++++++++++++++ .../src/api/middleware/ServiceMiddleware.ts | 1 + 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/fluxer_api/src/api/auth/AuthController.ts b/fluxer_api/src/api/auth/AuthController.ts index a7f3f0246..6069a161d 100644 --- a/fluxer_api/src/api/auth/AuthController.ts +++ b/fluxer_api/src/api/auth/AuthController.ts @@ -102,7 +102,9 @@ export function AuthController(app: HonoApp) { 'Complete the SSO authentication flow with the authorization code from the SSO provider. Returns authentication token and user information.', }), async (ctx) => { - const result = await ctx.get('authRequestService').completeSso(ctx.req.valid('json'), ctx.req.raw); + const result = await ctx + .get('authRequestService') + .completeSso(ctx.req.valid('json'), ctx.req.raw, ctx.get('requestCache')); return ctx.json(result); }, ); diff --git a/fluxer_api/src/api/auth/AuthRequestService.ts b/fluxer_api/src/api/auth/AuthRequestService.ts index 8e21af895..d30f2dd8d 100644 --- a/fluxer_api/src/api/auth/AuthRequestService.ts +++ b/fluxer_api/src/api/auth/AuthRequestService.ts @@ -168,8 +168,10 @@ export class AuthRequestService { }); } - completeSso(data: SsoCompleteRequest, request: Request) { - return this.toSsoCompleteResponse(this.ssoService.completeLogin({code: data.code, state: data.state, request})); + completeSso(data: SsoCompleteRequest, request: Request, requestCache: RequestCache) { + return this.toSsoCompleteResponse( + this.ssoService.completeLogin({code: data.code, state: data.state, request, requestCache}), + ); } async register({data, request, requestCache}: AuthRegisterRequest): Promise { diff --git a/fluxer_api/src/api/auth/services/SsoService.ts b/fluxer_api/src/api/auth/services/SsoService.ts index de7988467..65683c348 100644 --- a/fluxer_api/src/api/auth/services/SsoService.ts +++ b/fluxer_api/src/api/auth/services/SsoService.ts @@ -20,6 +20,7 @@ import { type InstanceSsoConfig, REGISTRATION_PENDING_APPROVAL_TRAIT, } from '@app/api/instance/InstanceConfigRepository'; +import type {SingleCommunityService} from '@app/api/instance/SingleCommunityService'; import { deriveSsoRedirectUri, getSsoRequestUrlPolicy, @@ -28,6 +29,7 @@ import { } from '@app/api/instance/SsoConfigValidation'; import {Logger} from '@app/api/Logger'; import {profileSubstringBlocklistCache} from '@app/api/middleware/ProfileSubstringBlocklistCache'; +import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {User} from '@app/api/models/User'; import {UserSettings} from '@app/api/models/UserSettings'; import {EXTERNAL_RESPONSE_LIMITS} from '@app/api/utils/ExternalResponseLimits'; @@ -260,6 +262,7 @@ export class SsoService { private readonly instanceConfigRepository: InstanceConfigRepository, private readonly discriminatorService: IDiscriminatorService, private readonly kvActivityTracker: KVActivityTracker, + private readonly singleCommunityService: SingleCommunityService, ) {} async getPublicStatus(): Promise { @@ -326,7 +329,17 @@ export class SsoService { return {authorization_url: authorizationUrlString, state, redirect_uri: config.redirectUri}; } - async completeLogin({code, state, request}: {code: string; state: string; request: Request}): Promise<{ + async completeLogin({ + code, + state, + request, + requestCache, + }: { + code: string; + state: string; + request: Request; + requestCache: RequestCache; + }): Promise<{ token: string; user_id: string; redirect_to: string; @@ -344,7 +357,7 @@ export class SsoService { config, }); const claims = await this.resolveClaims(tokenResponse, config, statePayload.nonce); - const user = await this.resolveUserFromClaims(claims, config); + const user = await this.resolveUserFromClaims(claims, config, requestCache); const [token] = await AuthSession.createAuthSession(this.apiContext, { user, origin: AuthSession.resolveSessionOrigin(this.apiContext, request), @@ -352,7 +365,11 @@ export class SsoService { return {token, user_id: user.id.toString(), redirect_to: statePayload.redirectTo ?? ''}; } - private async resolveUserFromClaims(claims: ResolvedSsoClaims, config: ResolvedSsoConfig): Promise { + private async resolveUserFromClaims( + claims: ResolvedSsoClaims, + config: ResolvedSsoConfig, + requestCache: RequestCache, + ): Promise { if (!claims.emailVerified) { throw InputValidationError.fromCode('email_verified', ValidationErrorCodes.INVALID_SSO_TOKEN); } @@ -388,6 +405,7 @@ export class SsoService { if (pendingApproval) { throw new RegistrationPendingApprovalError(); } + await this.singleCommunityService.joinStockCommunity(user.id, requestCache); return user; } diff --git a/fluxer_api/src/api/auth/tests/SsoFlow.test.ts b/fluxer_api/src/api/auth/tests/SsoFlow.test.ts index 3a1259320..29d3110f4 100644 --- a/fluxer_api/src/api/auth/tests/SsoFlow.test.ts +++ b/fluxer_api/src/api/auth/tests/SsoFlow.test.ts @@ -9,6 +9,8 @@ import { setUserACLs, type TestAccount, } from '@app/api/auth/tests/AuthTestUtils'; +import {setupTestGuildWithMembers} from '@app/api/guild/tests/GuildTestUtils'; +import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it} from 'vitest'; @@ -544,6 +546,29 @@ describe('Auth SSO flow', () => { .expect(403) .execute(); }); + it('joins a provisioned user to the single community', async () => { + const {owner, guild} = await setupTestGuildWithMembers(harness, 0); + await getInstanceConfigRepository().setInstancePolicyConfig({ + single_community_enabled: true, + single_community_guild_id: guild.id, + }); + await enableSso(harness, admin.token); + const startData = await createBuilderWithoutAuth(harness) + .post('/auth/sso/start') + .body({}) + .execute(); + const completeData = await createBuilderWithoutAuth(harness) + .post('/auth/sso/complete') + .body({ + code: `sso-single-community-${Date.now()}@example.com`, + state: startData.state, + }) + .execute(); + await createBuilder(harness, owner.token) + .get(`/guilds/${guild.id}/members/${completeData.user_id}`) + .expect(200) + .execute(); + }); }); describe('existing user login', () => { let admin: TestAccount; diff --git a/fluxer_api/src/api/middleware/ServiceMiddleware.ts b/fluxer_api/src/api/middleware/ServiceMiddleware.ts index d9ad6ff01..61502ad15 100644 --- a/fluxer_api/src/api/middleware/ServiceMiddleware.ts +++ b/fluxer_api/src/api/middleware/ServiceMiddleware.ts @@ -541,6 +541,7 @@ class RequestServices implements RequestScopedServices { getInstanceConfigRepository(), getDiscriminatorService(), getKVActivityTracker(), + this.singleCommunityService, ); return this.cachedSsoService; }