From facf6ee28b8c40fa8fbf48f08b8cc2a940ac3c17 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Wed, 2 Jul 2025 16:07:34 +0800 Subject: [PATCH] fix(server): disable Apple oauth on client version < 0.22.0 (#12984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close AF-2705 #### PR Dependency Tree * **PR #12984** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit * **New Features** * The Apple OAuth provider is now available only for clients version 0.22.0 or higher. * Client version detection has been improved by extracting version information from request headers. * **Bug Fixes** * Ensured that the Apple OAuth provider is hidden for clients below version 0.22.0. * **Tests** * Added comprehensive end-to-end and utility tests for OAuth provider selection and client version extraction. --- .../server/src/__tests__/e2e/create-app.ts | 5 +- .../oauth/__snapshots__/resolver.spec.ts.md | 76 ++++++++++++ .../oauth/__snapshots__/resolver.spec.ts.snap | Bin 0 -> 403 bytes .../src/__tests__/e2e/oauth/resolver.spec.ts | 111 ++++++++++++++++++ .../src/base/utils/__tests__/request.spec.ts | 33 ++++++ .../backend/server/src/base/utils/request.ts | 8 ++ .../server/src/plugins/oauth/resolver.ts | 27 ++++- 7 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 packages/backend/server/src/__tests__/e2e/oauth/__snapshots__/resolver.spec.ts.md create mode 100644 packages/backend/server/src/__tests__/e2e/oauth/__snapshots__/resolver.spec.ts.snap create mode 100644 packages/backend/server/src/__tests__/e2e/oauth/resolver.spec.ts create mode 100644 packages/backend/server/src/base/utils/__tests__/request.spec.ts diff --git a/packages/backend/server/src/__tests__/e2e/create-app.ts b/packages/backend/server/src/__tests__/e2e/create-app.ts index 78bf5f467..29ab9b3e8 100644 --- a/packages/backend/server/src/__tests__/e2e/create-app.ts +++ b/packages/backend/server/src/__tests__/e2e/create-app.ts @@ -1,7 +1,7 @@ import assert from 'node:assert'; import { gqlFetcherFactory } from '@affine/graphql'; -import { INestApplication } from '@nestjs/common'; +import { INestApplication, ModuleMetadata } from '@nestjs/common'; import { NestApplication } from '@nestjs/core'; import { Test, TestingModuleBuilder } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; @@ -35,6 +35,7 @@ import { parseCookies, TEST_LOG_LEVEL } from '../utils'; interface TestingAppMetadata { tapModule?(m: TestingModuleBuilder): void; tapApp?(app: INestApplication): void; + imports?: ModuleMetadata['imports']; } export class TestingApp extends NestApplication { @@ -203,7 +204,7 @@ export async function createApp( const { tapModule, tapApp } = metadata; const builder = Test.createTestingModule({ - imports: [buildAppModule(globalThis.env)], + imports: [buildAppModule(globalThis.env), ...(metadata.imports ?? [])], }); builder.overrideProvider(Mailer).useValue(new MockMailer()); diff --git a/packages/backend/server/src/__tests__/e2e/oauth/__snapshots__/resolver.spec.ts.md b/packages/backend/server/src/__tests__/e2e/oauth/__snapshots__/resolver.spec.ts.md new file mode 100644 index 000000000..11a55a434 --- /dev/null +++ b/packages/backend/server/src/__tests__/e2e/oauth/__snapshots__/resolver.spec.ts.md @@ -0,0 +1,76 @@ +# Snapshot report for `src/__tests__/e2e/oauth/resolver.spec.ts` + +The actual snapshot is saved in `resolver.spec.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## should return apple oauth provider in version >= 0.22.0 + +> Snapshot 1 + + { + serverConfig: { + oauthProviders: [ + 'Google', + 'Apple', + ], + }, + } + +> Snapshot 2 + + { + serverConfig: { + oauthProviders: [ + 'Google', + 'Apple', + ], + }, + } + +> Snapshot 3 + + { + serverConfig: { + oauthProviders: [ + 'Google', + 'Apple', + ], + }, + } + +## should not return apple oauth provider when client version is not specified + +> Snapshot 1 + + { + serverConfig: { + oauthProviders: [ + 'Google', + ], + }, + } + +## should not return apple oauth provider in version < 0.22.0 + +> Snapshot 1 + + { + serverConfig: { + oauthProviders: [ + 'Google', + ], + }, + } + +## should not return apple oauth provider when client version format is not correct + +> Snapshot 1 + + { + serverConfig: { + oauthProviders: [ + 'Google', + ], + }, + } diff --git a/packages/backend/server/src/__tests__/e2e/oauth/__snapshots__/resolver.spec.ts.snap b/packages/backend/server/src/__tests__/e2e/oauth/__snapshots__/resolver.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..13e939c92d3da453e673252d8364af5e9ae533dd GIT binary patch literal 403 zcmV;E0c`$3RzV8*dL1s00000000B+l+Q}TKoEz2lh7(s`-l1f`vj#FK@b&+2M-=Bp1h{Xrdf@< zVRu{YNkrd6>a%zd559v3-@=0=B(*42!IQLm8V2@*`Iz5K!hWp$Gjn^-t*|k_+%l0G z#sSmZ4z*-nlEj=TZ)hWyXf+Z6*Gxz@;@Su$+1@T|w>q6xTNvpjCQ{biOoG6(-pQMb z6{L?=$!P)D1kj{%gUUz!Q|?=$X<4t=LF*s_Rf&!PY>*2oHK<~^9&x>|WGDt1Y7F4& zGs>BRx|=~&voU)46efkaj{u$lycWPxS8H!fZc){0HP9N!0aef>XG$v{C^d+A-X<#g zrEaQh>0s#~bx_3WFprf~_SEd+I6n@ { + app = await createApp({ + imports: [ + ConfigModule.override({ + oauth: { + providers: { + apple: { + clientId: 'test', + clientSecret: 'test', + args: { + redirectUri: 'test', + }, + }, + google: { + clientId: 'test', + clientSecret: 'test', + args: { + redirectUri: 'test', + }, + }, + }, + }, + }), + ], + }); +}); + +e2e.after.always(async () => { + await app.close(); +}); + +e2e('should return apple oauth provider in version >= 0.22.0', async t => { + const res = await app.gql({ + query: oauthProvidersQuery, + context: { + headers: { + 'x-affine-version': '0.22.0', + }, + }, + }); + t.snapshot(res); + + const res2 = await app.gql({ + query: oauthProvidersQuery, + context: { + headers: { + 'x-affine-version': '0.23.0-beta.1', + }, + }, + }); + + t.snapshot(res2); + + const res3 = await app.gql({ + query: oauthProvidersQuery, + context: { + headers: { + 'x-affine-version': '2025.6.29-canary.93', + }, + }, + }); + + t.snapshot(res3); +}); + +e2e( + 'should not return apple oauth provider when client version is not specified', + async t => { + const res = await app.gql({ + query: oauthProvidersQuery, + }); + + t.snapshot(res); + } +); + +e2e('should not return apple oauth provider in version < 0.22.0', async t => { + const res = await app.gql({ + query: oauthProvidersQuery, + context: { + headers: { + 'x-affine-version': '0.21.0', + }, + }, + }); + + t.snapshot(res); +}); + +e2e( + 'should not return apple oauth provider when client version format is not correct', + async t => { + const res = await app.gql({ + query: oauthProvidersQuery, + context: { + headers: { + 'x-affine-version': 'mock-invalid-version', + }, + }, + }); + + t.snapshot(res); + } +); diff --git a/packages/backend/server/src/base/utils/__tests__/request.spec.ts b/packages/backend/server/src/base/utils/__tests__/request.spec.ts new file mode 100644 index 000000000..6bd1a522e --- /dev/null +++ b/packages/backend/server/src/base/utils/__tests__/request.spec.ts @@ -0,0 +1,33 @@ +import test from 'ava'; +import { Request } from 'express'; + +import { getClientVersionFromRequest } from '../request'; + +test('should get client version from x-affine-version header', t => { + const req = { + headers: { + 'x-affine-version': '0.22.2', + }, + } as unknown as Request; + + t.is(getClientVersionFromRequest(req), '0.22.2'); + + const req2 = { + headers: { + 'x-affine-version': ['0.22.2', '0.23.0-beta.2'], + }, + } as unknown as Request; + + t.is(getClientVersionFromRequest(req2), '0.22.2'); +}); + +test('should not get client version from x-affine-version header', t => { + const req = { + headers: { + 'user-agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.205 Safari/537.36', + }, + } as unknown as Request; + + t.is(getClientVersionFromRequest(req), undefined); +}); diff --git a/packages/backend/server/src/base/utils/request.ts b/packages/backend/server/src/base/utils/request.ts index af71b7830..a4da6a565 100644 --- a/packages/backend/server/src/base/utils/request.ts +++ b/packages/backend/server/src/base/utils/request.ts @@ -118,3 +118,11 @@ export function getRequestIdFromHost(host: ArgumentsHost) { const req = getRequestFromHost(host); return getRequestIdFromRequest(req, type); } + +export function getClientVersionFromRequest(req: Request) { + let version = req.headers['x-affine-version']; + if (Array.isArray(version)) { + version = version[0]; + } + return version; +} diff --git a/packages/backend/server/src/plugins/oauth/resolver.ts b/packages/backend/server/src/plugins/oauth/resolver.ts index b69d8f6e1..f0bab4161 100644 --- a/packages/backend/server/src/plugins/oauth/resolver.ts +++ b/packages/backend/server/src/plugins/oauth/resolver.ts @@ -1,17 +1,38 @@ -import { registerEnumType, ResolveField, Resolver } from '@nestjs/graphql'; +import { + Context, + registerEnumType, + ResolveField, + Resolver, +} from '@nestjs/graphql'; +import type { Request } from 'express'; +import semver from 'semver'; +import { getClientVersionFromRequest } from '../../base'; import { ServerConfigType } from '../../core/config/types'; import { OAuthProviderName } from './config'; import { OAuthProviderFactory } from './factory'; registerEnumType(OAuthProviderName, { name: 'OAuthProviderType' }); +const APPLE_OAUTH_PROVIDER_MIN_VERSION = new semver.Range('>=0.22.0', { + includePrerelease: true, +}); + @Resolver(() => ServerConfigType) export class OAuthResolver { constructor(private readonly factory: OAuthProviderFactory) {} @ResolveField(() => [OAuthProviderName]) - oauthProviders() { - return this.factory.providers; + oauthProviders(@Context() ctx: { req: Request }) { + // Apple oauth provider is not supported in client version < 0.22.0 + const providers = this.factory.providers; + if (providers.includes(OAuthProviderName.Apple)) { + const version = getClientVersionFromRequest(ctx.req); + if (!version || !APPLE_OAUTH_PROVIDER_MIN_VERSION.test(version)) { + return providers.filter(p => p !== OAuthProviderName.Apple); + } + } + + return providers; } }