From fc76163dd1895784e95f19da1cbbfef20661dff4 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Tue, 12 Sep 2023 13:31:58 +0800 Subject: [PATCH] fix: get auth token for development (#4295) --- apps/core/src/pages/open-app.tsx | 4 +- apps/server/src/modules/auth/resolver.ts | 29 ++++- apps/server/src/modules/auth/service.ts | 13 -- apps/server/src/schema.gql | 1 + apps/server/src/tests/auth.spec.ts | 112 ++++++++++++++++-- .../graphql/src/graphql/get-current-user.gql | 2 +- packages/graphql/src/graphql/index.ts | 2 +- packages/graphql/src/schema.ts | 2 +- 8 files changed, 131 insertions(+), 34 deletions(-) diff --git a/apps/core/src/pages/open-app.tsx b/apps/core/src/pages/open-app.tsx index 59249d4445..f452d6b165 100644 --- a/apps/core/src/pages/open-app.tsx +++ b/apps/core/src/pages/open-app.tsx @@ -178,11 +178,11 @@ const OpenOAuthJwt = () => { }, [params]); const channel = schemaToChanel[schema as Schema]; - if (!currentUser || !currentUser?.token?.token) { + if (!currentUser || !currentUser?.token?.sessionToken) { return null; } - const urlToOpen = `${schema}://oauth-jwt?token=${currentUser.token.token}`; + const urlToOpen = `${schema}://oauth-jwt?token=${currentUser.token.sessionToken}`; return ; }; diff --git a/apps/server/src/modules/auth/resolver.ts b/apps/server/src/modules/auth/resolver.ts index 581b582e0e..dfd8e8a061 100644 --- a/apps/server/src/modules/auth/resolver.ts +++ b/apps/server/src/modules/auth/resolver.ts @@ -30,6 +30,9 @@ export class TokenType { @Field() refresh!: string; + + @Field({ nullable: true }) + sessionToken?: string; } /** @@ -49,18 +52,32 @@ export class AuthResolver { @Throttle(20, 60) @ResolveField(() => TokenType) - async token(@CurrentUser() currentUser: UserType, @Parent() user: UserType) { + async token( + @Context() ctx: { req: Request }, + @CurrentUser() currentUser: UserType, + @Parent() user: UserType + ) { if (user.id !== currentUser.id) { throw new BadRequestException('Invalid user'); } - // on production we use session token that is stored in database (strategy = 'database') - const sessionToken = this.config.node.prod - ? await this.auth.getSessionToken(user.id) - : this.auth.sign(user); + let sessionToken: string | undefined; + + // only return session if the request is from the same origin & path == /open-app + if ( + ctx.req.headers.referer && + ctx.req.headers.host && + new URL(ctx.req.headers.referer).pathname.startsWith('/open-app') && + ctx.req.headers.host === new URL(this.config.origin).host + ) { + const cookiePrefix = this.config.node.prod ? '__Secure-' : ''; + const sessionCookieName = `${cookiePrefix}next-auth.session-token`; + sessionToken = ctx.req.cookies?.[sessionCookieName]; + } return { - token: sessionToken, + sessionToken, + token: this.auth.sign(user), refresh: this.auth.refresh(user), }; } diff --git a/apps/server/src/modules/auth/service.ts b/apps/server/src/modules/auth/service.ts index ad126092e9..7ac7828ddb 100644 --- a/apps/server/src/modules/auth/service.ts +++ b/apps/server/src/modules/auth/service.ts @@ -251,17 +251,4 @@ export class AuthService { async sendChangeEmail(email: string, callbackUrl: string) { return this.mailer.sendChangeEmail(email, callbackUrl); } - async getSessionToken(userId: string) { - const session = await this.prisma.session.findFirst({ - where: { - userId: userId, - }, - }); - - if (!session) { - throw new BadRequestException(`No session found for user id ${userId}`); - } - - return session?.sessionToken; - } } diff --git a/apps/server/src/schema.gql b/apps/server/src/schema.gql index d652079964..2fd95a2854 100644 --- a/apps/server/src/schema.gql +++ b/apps/server/src/schema.gql @@ -48,6 +48,7 @@ enum NewFeaturesKind { type TokenType { token: String! refresh: String! + sessionToken: String } type InviteUserType { diff --git a/apps/server/src/tests/auth.spec.ts b/apps/server/src/tests/auth.spec.ts index 1f0816534f..93ea72ecbc 100644 --- a/apps/server/src/tests/auth.spec.ts +++ b/apps/server/src/tests/auth.spec.ts @@ -7,11 +7,13 @@ import { ConfigModule } from '../config'; import { GqlModule } from '../graphql.module'; import { MetricsModule } from '../metrics'; import { AuthModule } from '../modules/auth'; +import { AuthResolver } from '../modules/auth/resolver'; import { AuthService } from '../modules/auth/service'; import { PrismaModule } from '../prisma'; import { RateLimiterModule } from '../throttler'; -let auth: AuthService; +let authService: AuthService; +let authResolver: AuthResolver; let module: TestingModule; // cleanup database before each test @@ -31,6 +33,8 @@ test.beforeEach(async () => { refreshTokenExpiresIn: 1, leeway: 1, }, + host: 'example.org', + https: true, }), PrismaModule, GqlModule, @@ -39,7 +43,8 @@ test.beforeEach(async () => { RateLimiterModule, ], }).compile(); - auth = module.get(AuthService); + authService = module.get(AuthService); + authResolver = module.get(AuthResolver); }); test.afterEach.always(async () => { @@ -47,14 +52,14 @@ test.afterEach.always(async () => { }); test('should be able to register and signIn', async t => { - await auth.signUp('Alex Yang', 'alexyang@example.org', '123456'); - await auth.signIn('alexyang@example.org', '123456'); + await authService.signUp('Alex Yang', 'alexyang@example.org', '123456'); + await authService.signIn('alexyang@example.org', '123456'); t.pass(); }); test('should be able to verify', async t => { - await auth.signUp('Alex Yang', 'alexyang@example.org', '123456'); - await auth.signIn('alexyang@example.org', '123456'); + await authService.signUp('Alex Yang', 'alexyang@example.org', '123456'); + await authService.signIn('alexyang@example.org', '123456'); const date = new Date(); const user = { @@ -66,8 +71,8 @@ test('should be able to verify', async t => { avatarUrl: '', }; { - const token = await auth.sign(user); - const claim = await auth.verify(token); + const token = await authService.sign(user); + const claim = await authService.verify(token); t.is(claim.id, '1'); t.is(claim.name, 'Alex Yang'); t.is(claim.email, 'alexyang@example.org'); @@ -75,8 +80,8 @@ test('should be able to verify', async t => { t.is(claim.createdAt.toISOString(), date.toISOString()); } { - const token = await auth.refresh(user); - const claim = await auth.verify(token); + const token = await authService.refresh(user); + const claim = await authService.verify(token); t.is(claim.id, '1'); t.is(claim.name, 'Alex Yang'); t.is(claim.email, 'alexyang@example.org'); @@ -84,3 +89,90 @@ test('should be able to verify', async t => { t.is(claim.createdAt.toISOString(), date.toISOString()); } }); + +test('should not be able to return token if user is invalid', async t => { + const date = new Date(); + const user = { + id: '1', + name: 'Alex Yang', + email: 'alexyang@example.org', + emailVerified: date, + createdAt: date, + avatarUrl: '', + }; + const anotherUser = { + id: '2', + name: 'Alex Yang 2', + email: 'alexyang@example.org', + emailVerified: date, + createdAt: date, + avatarUrl: '', + }; + await t.throwsAsync( + authResolver.token( + { + req: { + headers: { + referer: 'https://example.org', + host: 'example.org', + }, + } as any, + }, + user, + anotherUser + ), + { + message: 'Invalid user', + } + ); +}); + +test('should not return sessionToken if request headers is invalid', async t => { + const date = new Date(); + const user = { + id: '1', + name: 'Alex Yang', + email: 'alexyang@example.org', + emailVerified: date, + createdAt: date, + avatarUrl: '', + }; + const result = await authResolver.token( + { + req: { + headers: {}, + } as any, + }, + user, + user + ); + t.is(result.sessionToken, undefined); +}); + +test('should return valid sessionToken if request headers valid', async t => { + const date = new Date(); + const user = { + id: '1', + name: 'Alex Yang', + email: 'alexyang@example.org', + emailVerified: date, + createdAt: date, + avatarUrl: '', + }; + const result = await authResolver.token( + { + req: { + headers: { + referer: 'https://example.org/open-app/test', + host: 'example.org', + }, + cookies: { + 'next-auth.session-token': '123456', + }, + } as any, + }, + user, + user + ); + t.is(result.sessionToken, '123456'); +}); diff --git a/packages/graphql/src/graphql/get-current-user.gql b/packages/graphql/src/graphql/get-current-user.gql index 385592eaf5..272f527e04 100644 --- a/packages/graphql/src/graphql/get-current-user.gql +++ b/packages/graphql/src/graphql/get-current-user.gql @@ -7,7 +7,7 @@ query getCurrentUser { avatarUrl createdAt token { - token + sessionToken } } } diff --git a/packages/graphql/src/graphql/index.ts b/packages/graphql/src/graphql/index.ts index 5a16eab4d7..8e60bb7fff 100644 --- a/packages/graphql/src/graphql/index.ts +++ b/packages/graphql/src/graphql/index.ts @@ -165,7 +165,7 @@ query getCurrentUser { avatarUrl createdAt token { - token + sessionToken } } }`, diff --git a/packages/graphql/src/schema.ts b/packages/graphql/src/schema.ts index e772d0c847..51e941e2e2 100644 --- a/packages/graphql/src/schema.ts +++ b/packages/graphql/src/schema.ts @@ -173,7 +173,7 @@ export type GetCurrentUserQuery = { emailVerified: string | null; avatarUrl: string | null; createdAt: string | null; - token: { __typename?: 'TokenType'; token: string }; + token: { __typename?: 'TokenType'; sessionToken: string | null }; }; };