mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(sso): join provisioned users to the single community (#3118)
This commit is contained in:
@@ -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> {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user