fix(sso): join provisioned users to the single community (#3118)

This commit is contained in:
Hampus
2026-10-02 17:23:03 +02:00
committed by GitHub
parent 87df92e2c2
commit 5040ae2c10
5 changed files with 54 additions and 6 deletions
+3 -1
View File
@@ -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.', 'Complete the SSO authentication flow with the authorization code from the SSO provider. Returns authentication token and user information.',
}), }),
async (ctx) => { 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); return ctx.json(result);
}, },
); );
@@ -168,8 +168,10 @@ export class AuthRequestService {
}); });
} }
completeSso(data: SsoCompleteRequest, request: Request) { completeSso(data: SsoCompleteRequest, request: Request, requestCache: RequestCache) {
return this.toSsoCompleteResponse(this.ssoService.completeLogin({code: data.code, state: data.state, request})); return this.toSsoCompleteResponse(
this.ssoService.completeLogin({code: data.code, state: data.state, request, requestCache}),
);
} }
async register({data, request, requestCache}: AuthRegisterRequest): Promise<AuthRegisterResponse> { async register({data, request, requestCache}: AuthRegisterRequest): Promise<AuthRegisterResponse> {
+21 -3
View File
@@ -20,6 +20,7 @@ import {
type InstanceSsoConfig, type InstanceSsoConfig,
REGISTRATION_PENDING_APPROVAL_TRAIT, REGISTRATION_PENDING_APPROVAL_TRAIT,
} from '@app/api/instance/InstanceConfigRepository'; } from '@app/api/instance/InstanceConfigRepository';
import type {SingleCommunityService} from '@app/api/instance/SingleCommunityService';
import { import {
deriveSsoRedirectUri, deriveSsoRedirectUri,
getSsoRequestUrlPolicy, getSsoRequestUrlPolicy,
@@ -28,6 +29,7 @@ import {
} from '@app/api/instance/SsoConfigValidation'; } from '@app/api/instance/SsoConfigValidation';
import {Logger} from '@app/api/Logger'; import {Logger} from '@app/api/Logger';
import {profileSubstringBlocklistCache} from '@app/api/middleware/ProfileSubstringBlocklistCache'; import {profileSubstringBlocklistCache} from '@app/api/middleware/ProfileSubstringBlocklistCache';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {User} from '@app/api/models/User'; import type {User} from '@app/api/models/User';
import {UserSettings} from '@app/api/models/UserSettings'; import {UserSettings} from '@app/api/models/UserSettings';
import {EXTERNAL_RESPONSE_LIMITS} from '@app/api/utils/ExternalResponseLimits'; import {EXTERNAL_RESPONSE_LIMITS} from '@app/api/utils/ExternalResponseLimits';
@@ -260,6 +262,7 @@ export class SsoService {
private readonly instanceConfigRepository: InstanceConfigRepository, private readonly instanceConfigRepository: InstanceConfigRepository,
private readonly discriminatorService: IDiscriminatorService, private readonly discriminatorService: IDiscriminatorService,
private readonly kvActivityTracker: KVActivityTracker, private readonly kvActivityTracker: KVActivityTracker,
private readonly singleCommunityService: SingleCommunityService,
) {} ) {}
async getPublicStatus(): Promise<PublicSsoStatus> { async getPublicStatus(): Promise<PublicSsoStatus> {
@@ -326,7 +329,17 @@ export class SsoService {
return {authorization_url: authorizationUrlString, state, redirect_uri: config.redirectUri}; 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; token: string;
user_id: string; user_id: string;
redirect_to: string; redirect_to: string;
@@ -344,7 +357,7 @@ export class SsoService {
config, config,
}); });
const claims = await this.resolveClaims(tokenResponse, config, statePayload.nonce); 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, { const [token] = await AuthSession.createAuthSession(this.apiContext, {
user, user,
origin: AuthSession.resolveSessionOrigin(this.apiContext, request), origin: AuthSession.resolveSessionOrigin(this.apiContext, request),
@@ -352,7 +365,11 @@ export class SsoService {
return {token, user_id: user.id.toString(), redirect_to: statePayload.redirectTo ?? ''}; return {token, user_id: user.id.toString(), redirect_to: statePayload.redirectTo ?? ''};
} }
private async resolveUserFromClaims(claims: ResolvedSsoClaims, config: ResolvedSsoConfig): Promise<User> { private async resolveUserFromClaims(
claims: ResolvedSsoClaims,
config: ResolvedSsoConfig,
requestCache: RequestCache,
): Promise<User> {
if (!claims.emailVerified) { if (!claims.emailVerified) {
throw InputValidationError.fromCode('email_verified', ValidationErrorCodes.INVALID_SSO_TOKEN); throw InputValidationError.fromCode('email_verified', ValidationErrorCodes.INVALID_SSO_TOKEN);
} }
@@ -388,6 +405,7 @@ export class SsoService {
if (pendingApproval) { if (pendingApproval) {
throw new RegistrationPendingApprovalError(); throw new RegistrationPendingApprovalError();
} }
await this.singleCommunityService.joinStockCommunity(user.id, requestCache);
return user; return user;
} }
@@ -9,6 +9,8 @@ import {
setUserACLs, setUserACLs,
type TestAccount, type TestAccount,
} from '@app/api/auth/tests/AuthTestUtils'; } 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 type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it} from 'vitest'; import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it} from 'vitest';
@@ -544,6 +546,29 @@ describe('Auth SSO flow', () => {
.expect(403) .expect(403)
.execute(); .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<SsoStartResponse>(harness)
.post('/auth/sso/start')
.body({})
.execute();
const completeData = await createBuilderWithoutAuth<SsoCompleteResponse>(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', () => { describe('existing user login', () => {
let admin: TestAccount; let admin: TestAccount;
@@ -541,6 +541,7 @@ class RequestServices implements RequestScopedServices {
getInstanceConfigRepository(), getInstanceConfigRepository(),
getDiscriminatorService(), getDiscriminatorService(),
getKVActivityTracker(), getKVActivityTracker(),
this.singleCommunityService,
); );
return this.cachedSsoService; return this.cachedSsoService;
} }