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.',
}),
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);
},
);
@@ -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<AuthRegisterResponse> {
+21 -3
View File
@@ -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<PublicSsoStatus> {
@@ -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<User> {
private async resolveUserFromClaims(
claims: ResolvedSsoClaims,
config: ResolvedSsoConfig,
requestCache: RequestCache,
): Promise<User> {
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;
}
@@ -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<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', () => {
let admin: TestAccount;
@@ -541,6 +541,7 @@ class RequestServices implements RequestScopedServices {
getInstanceConfigRepository(),
getDiscriminatorService(),
getKVActivityTracker(),
this.singleCommunityService,
);
return this.cachedSsoService;
}