From fbe9b1dd6e461583a184aac47c0db9a05971f4a5 Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 2 Jan 2024 17:29:21 +0200 Subject: [PATCH 01/11] fix: accessing length of undefined --- .../auth/guards/organization-roles/organization-roles.guard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts b/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts index 0065f07c74bf71..80ca2779d0db34 100644 --- a/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts @@ -10,7 +10,7 @@ export class OrganizationRolesGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const requiredRoles = this.reflector.get(Roles, context.getHandler()); - if (!requiredRoles.length || !Object.keys(requiredRoles).length) { + if (!requiredRoles?.length || !Object.keys(requiredRoles)?.length) { return true; } From da81cd675d67eebd51619fd6e9d2bdf74d0a11aa Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 2 Jan 2024 17:34:38 +0200 Subject: [PATCH 02/11] refactor: GetUser throw error if no user provided --- .../modules/auth/decorators/get-user/get-user.decorator.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/api/v2/src/modules/auth/decorators/get-user/get-user.decorator.ts b/apps/api/v2/src/modules/auth/decorators/get-user/get-user.decorator.ts index edbaef2081ce73..28ec9d5e3a34ed 100644 --- a/apps/api/v2/src/modules/auth/decorators/get-user/get-user.decorator.ts +++ b/apps/api/v2/src/modules/auth/decorators/get-user/get-user.decorator.ts @@ -6,6 +6,10 @@ export const GetUser = createParamDecorator { return { From aa7b29ad1ead0e24b976c9f6de80aa24d04281fd Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 2 Jan 2024 18:13:55 +0200 Subject: [PATCH 03/11] fix: cascade delete PlatformAuthorizationToken if owner or client deleted --- packages/prisma/schema.prisma | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 73fc2bd955aa3e..1a593a807af9b5 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -1067,8 +1067,8 @@ model PlatformOAuthClient { model PlatformAuthorizationToken { id String @id @default(cuid()) - owner User @relation(fields: [userId], references: [id]) - client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id]) + owner User @relation(fields: [userId], references: [id], onDelete: Cascade) + client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade) platformOAuthClientId String @map("platform_oauth_client_id") userId Int @map("user_id") From cb4d3beba73de65337857c5736af7ff2ac4b72b4 Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 2 Jan 2024 18:21:20 +0200 Subject: [PATCH 04/11] test: POST /authorize --- .../oauth-flow.controller.e2e-spec.ts | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts new file mode 100644 index 00000000000000..bf2e16fcc49fb3 --- /dev/null +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts @@ -0,0 +1,144 @@ +import { AppModule } from "@/app.module"; +import { HttpExceptionFilter } from "@/filters/http-exception.filter"; +import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; +import { ZodExceptionFilter } from "@/filters/zod-exception.filter"; +import { AuthModule } from "@/modules/auth/auth.module"; +import { OAuthAuthorizeInput } from "@/modules/oauth-clients/inputs/authorize.input"; +import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { Test, TestingModule } from "@nestjs/testing"; +import { Membership, PlatformOAuthClient, Team, User } from "@prisma/client"; +import * as request from "supertest"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; +import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { withNextAuth } from "test/utils/withNextAuth"; + +describe("OAuthFlow Endpoints", () => { + let app: INestApplication; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let testClient: PlatformOAuthClient; + const userEmail = "user@api.com"; + let authorizationCode: string | null; + let refreshToken: string; + let membership: Membership; + let organization: Team; + let teamRepositoryFixture: TeamRepositoryFixture; + let usersFixtures: UserRepositoryFixture; + let membershipFixtures: MembershipRepositoryFixture; + let user: User; + + beforeAll(async () => { + const moduleRef: TestingModule = await withNextAuth( + userEmail, + Test.createTestingModule({ + providers: [PrismaExceptionFilter, HttpExceptionFilter, ZodExceptionFilter], + imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule], + }) + ).compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + + // Setup OAuthClientRepositoryFixture + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + usersFixtures = new UserRepositoryFixture(moduleRef); + membershipFixtures = new MembershipRepositoryFixture(moduleRef); + + // Create a test OAuth client + user = await usersFixtures.create({ + email: userEmail, + }); + organization = await teamRepositoryFixture.create({ name: "organization" }); + membership = await membershipFixtures.addUserToOrg(user, organization, "OWNER", true); + testClient = await createOAuthClient(organization.id); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: ["redirect-uri"], + permissions: 32, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + + describe("Authorize Endpoint", () => { + it.only("POST /oauth/:clientId/authorize", async () => { + const body: OAuthAuthorizeInput = { + redirectUri: testClient.redirectUris[0], + }; + + const REDIRECT_STATUS = 302; + + const response = await request(app.getHttpServer()) + .post(`/oauth/${testClient.id}/authorize`) + .send(body) + .expect(REDIRECT_STATUS); + + console.log("asap response", JSON.stringify(response, null, 2)); + + const baseUrl = "http://www.localhost/"; + const redirectUri = new URL(response.header.location, baseUrl); + authorizationCode = redirectUri.searchParams.get("code"); + expect(authorizationCode).toBeDefined(); + }); + }); + + // describe('Exchange Endpoint', () => { + // it('POST /oauth/:clientId/exchange', async () => { + // const authorizationToken = 'Bearer exampleBearerToken'; + // const body = { + // clientSecret: testClient.secret, + // code: authorizationCode, + // }; + + // const response = await request(app.getHttpServer()) + // .post(`/oauth/${testClient.id}/exchange`) + // .set('Authorization', authorizationToken) + // .send(body) + // .expect(200); + + // expect(response.body).toHaveProperty('data'); + // expect(response.body.data).toHaveProperty('accessToken'); + // expect(response.body.data).toHaveProperty('refreshToken'); + // refreshToken = response.body.data.refreshToken; + // }); + // }); + + // describe('Refresh Token Endpoint', () => { + // it('POST /oauth/:clientId/refresh', () => { + // const secretKey = testClient.secret; + // const body = { + // refreshToken, + // }; + + // return request(app.getHttpServer()) + // .post(`/oauth/${testClient.id}/refresh`) + // .set('x-cal-secret-key', secretKey) + // .send(body) + // .expect(200) + // .then((response) => { + // expect(response.body).toHaveProperty('data'); + // expect(response.body.data).toHaveProperty('accessToken'); + // expect(response.body.data).toHaveProperty('refreshToken'); + // }); + // }); + // }); + + afterAll(async () => { + await oauthClientRepositoryFixture.delete(testClient.id); + await teamRepositoryFixture.delete(organization.id); + await usersFixtures.delete(user.id); + + await app.close(); + }); +}); From fe0739a4f0ed64b5b08a3e1b2cff9a26e5860247 Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 3 Jan 2024 08:51:30 +0200 Subject: [PATCH 05/11] refactor oauth-flow controller --- .../controllers/oauth-flow/oauth-flow.controller.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts index 5f7eba3bcb5577..30ac5651d344f9 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts @@ -65,13 +65,16 @@ export class OAuthFlowController { @Param("clientId") clientId: string, @Body() body: ExchangeAuthorizationCodeInput ): Promise> { - const bearerToken = authorization.replace("Bearer ", "").trim(); - if (!bearerToken) { + const authorizeEndpointCode = authorization.replace("Bearer ", "").trim(); + if (!authorizeEndpointCode) { throw new BadRequestException("Missing 'Bearer' Authorization header."); } - const { accessToken: accessToken, refreshToken: refreshToken } = - await this.oAuthFlowService.exchangeAuthorizationToken(bearerToken, clientId, body.clientSecret); + const { accessToken, refreshToken } = await this.oAuthFlowService.exchangeAuthorizationToken( + authorizeEndpointCode, + clientId, + body.clientSecret + ); return { status: SUCCESS_STATUS, From 1aadccfe60266c0681c28b82a6155f57768838d1 Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 3 Jan 2024 09:02:52 +0200 Subject: [PATCH 06/11] refactor oauth-flow controller --- .../controllers/oauth-flow/oauth-flow.controller.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts index 30ac5651d344f9..d4b678069d4afd 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts @@ -53,6 +53,17 @@ export class OAuthFlowController { throw new BadRequestException("Invalid 'redirect_uri' value."); } + const alreadyAuthorized = await this.tokensRepository.getAuthorizationTokenByClientUserIds( + clientId, + userId + ); + + if (alreadyAuthorized) { + throw new BadRequestException( + `User with id=${userId} has already authorized client with id=${clientId}.` + ); + } + const { id } = await this.tokensRepository.createAuthorizationToken(clientId, userId); return res.redirect(`${body.redirectUri}?code=${id}`); From 098dcdabe1004e7482dbcad3b666ea810846972a Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 3 Jan 2024 09:17:23 +0200 Subject: [PATCH 07/11] new function to get authorization token by client user ids --- apps/api/v2/src/modules/tokens/tokens.repository.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/api/v2/src/modules/tokens/tokens.repository.ts b/apps/api/v2/src/modules/tokens/tokens.repository.ts index 0ff6f12f630616..6af60668aef0cf 100644 --- a/apps/api/v2/src/modules/tokens/tokens.repository.ts +++ b/apps/api/v2/src/modules/tokens/tokens.repository.ts @@ -30,6 +30,15 @@ export class TokensRepository { }); } + async getAuthorizationTokenByClientUserIds(clientId: string, userId: number) { + return this.dbRead.prisma.platformAuthorizationToken.findFirst({ + where: { + platformOAuthClientId: clientId, + userId: userId, + }, + }); + } + async createOAuthTokens(clientId: string, ownerId: number) { const accessExpiry = DateTime.now().plus({ days: 1 }).startOf("day").toJSDate(); const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate(); From 4bd7531817a21e2c667342b2c2ef3c9c1d07793c Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 3 Jan 2024 09:22:27 +0200 Subject: [PATCH 08/11] refactor token service --- apps/api/v2/src/modules/tokens/tokens.repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/modules/tokens/tokens.repository.ts b/apps/api/v2/src/modules/tokens/tokens.repository.ts index 6af60668aef0cf..4708f87ff2f3ed 100644 --- a/apps/api/v2/src/modules/tokens/tokens.repository.ts +++ b/apps/api/v2/src/modules/tokens/tokens.repository.ts @@ -105,7 +105,7 @@ export class TokensRepository { this.dbWrite.prisma.refreshToken.delete({ where: { secret: refreshTokenSecret } }), this.dbWrite.prisma.accessToken.create({ data: { - secret: this.jwtService.sign(JSON.stringify({ type: "access_token", clientId: clientId })), + secret: this.jwtService.sign(JSON.stringify({ type: "access_token", clientId })), expiresAt: accessExpiry, client: { connect: { id: clientId } }, owner: { connect: { id: tokenUserId } }, @@ -113,7 +113,7 @@ export class TokensRepository { }), this.dbWrite.prisma.refreshToken.create({ data: { - secret: this.jwtService.sign(JSON.stringify({ type: "refresh_token", clientId: clientId })), + secret: this.jwtService.sign(JSON.stringify({ type: "refresh_token", clientId })), expiresAt: refreshExpiry, client: { connect: { id: clientId } }, owner: { connect: { id: tokenUserId } }, From 2b8112f0a3b9f668cec3c50182ed5897e0cf4b1a Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 3 Jan 2024 09:42:21 +0200 Subject: [PATCH 09/11] fix: re-created access and refresh tokens having not unique secret --- .../src/modules/tokens/tokens.repository.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/api/v2/src/modules/tokens/tokens.repository.ts b/apps/api/v2/src/modules/tokens/tokens.repository.ts index 4708f87ff2f3ed..484409010f9938 100644 --- a/apps/api/v2/src/modules/tokens/tokens.repository.ts +++ b/apps/api/v2/src/modules/tokens/tokens.repository.ts @@ -43,10 +43,14 @@ export class TokensRepository { const accessExpiry = DateTime.now().plus({ days: 1 }).startOf("day").toJSDate(); const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate(); + const issuedAtTime = Math.floor(Date.now() / 1000); + const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([ this.dbWrite.prisma.accessToken.create({ data: { - secret: this.jwtService.sign(JSON.stringify({ type: "access_token", clientId })), + secret: this.jwtService.sign( + JSON.stringify({ type: "access_token", clientId, ownerId, iat: issuedAtTime }) + ), expiresAt: accessExpiry, client: { connect: { id: clientId } }, owner: { connect: { id: ownerId } }, @@ -54,7 +58,9 @@ export class TokensRepository { }), this.dbWrite.prisma.refreshToken.create({ data: { - secret: this.jwtService.sign(JSON.stringify({ type: "refresh_token", clientId })), + secret: this.jwtService.sign( + JSON.stringify({ type: "refresh_token", clientId, ownerId, iat: issuedAtTime }) + ), expiresAt: refreshExpiry, client: { connect: { id: clientId } }, owner: { connect: { id: ownerId } }, @@ -97,6 +103,8 @@ export class TokensRepository { const accessExpiry = DateTime.now().plus({ days: 1 }).startOf("day").toJSDate(); const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate(); + const issuedAtTime = Math.floor(Date.now() / 1000); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, _refresh, accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([ this.dbWrite.prisma.accessToken.deleteMany({ @@ -105,7 +113,9 @@ export class TokensRepository { this.dbWrite.prisma.refreshToken.delete({ where: { secret: refreshTokenSecret } }), this.dbWrite.prisma.accessToken.create({ data: { - secret: this.jwtService.sign(JSON.stringify({ type: "access_token", clientId })), + secret: this.jwtService.sign( + JSON.stringify({ type: "access_token", clientId, userId: tokenUserId, iat: issuedAtTime }) + ), expiresAt: accessExpiry, client: { connect: { id: clientId } }, owner: { connect: { id: tokenUserId } }, @@ -113,7 +123,9 @@ export class TokensRepository { }), this.dbWrite.prisma.refreshToken.create({ data: { - secret: this.jwtService.sign(JSON.stringify({ type: "refresh_token", clientId })), + secret: this.jwtService.sign( + JSON.stringify({ type: "refresh_token", clientId, userId: tokenUserId, iat: issuedAtTime }) + ), expiresAt: refreshExpiry, client: { connect: { id: clientId } }, owner: { connect: { id: tokenUserId } }, From ddc6131296e6f6a659c2b1add68c641505816e6a Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 3 Jan 2024 11:26:56 +0200 Subject: [PATCH 10/11] oauth flow tests --- .../oauth-flow.controller.e2e-spec.ts | 251 ++++++++++-------- 1 file changed, 141 insertions(+), 110 deletions(-) diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts index bf2e16fcc49fb3..87c02f14d1a1c3 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts @@ -1,144 +1,175 @@ +import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; import { HttpExceptionFilter } from "@/filters/http-exception.filter"; import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; import { ZodExceptionFilter } from "@/filters/zod-exception.filter"; import { AuthModule } from "@/modules/auth/auth.module"; import { OAuthAuthorizeInput } from "@/modules/oauth-clients/inputs/authorize.input"; +import { ExchangeAuthorizationCodeInput } from "@/modules/oauth-clients/inputs/exchange-code.input"; import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { UsersModule } from "@/modules/users/users.module"; import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; import { Test, TestingModule } from "@nestjs/testing"; -import { Membership, PlatformOAuthClient, Team, User } from "@prisma/client"; +import { PlatformOAuthClient, Team, User } from "@prisma/client"; import * as request from "supertest"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { withNextAuth } from "test/utils/withNextAuth"; +import { X_CAL_SECRET_KEY } from "@calcom/platform-constants"; + describe("OAuthFlow Endpoints", () => { - let app: INestApplication; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let testClient: PlatformOAuthClient; - const userEmail = "user@api.com"; - let authorizationCode: string | null; - let refreshToken: string; - let membership: Membership; - let organization: Team; - let teamRepositoryFixture: TeamRepositoryFixture; - let usersFixtures: UserRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - let user: User; - - beforeAll(async () => { - const moduleRef: TestingModule = await withNextAuth( - userEmail, - Test.createTestingModule({ + describe("User Not Authenticated", () => { + let appWithoutAuth: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ providers: [PrismaExceptionFilter, HttpExceptionFilter, ZodExceptionFilter], imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule], - }) - ).compile(); + }).compile(); + appWithoutAuth = moduleRef.createNestApplication(); + bootstrap(appWithoutAuth as NestExpressApplication); + await appWithoutAuth.init(); + }); - app = moduleRef.createNestApplication(); - await app.init(); + it(`POST /oauth/:clientId/authorize missing Cookie with user`, () => { + return request(appWithoutAuth.getHttpServer()).post("/api/v2/oauth/100/authorize").expect(401); + }); - // Setup OAuthClientRepositoryFixture - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - usersFixtures = new UserRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); + it(`POST /oauth/:clientId/exchange missing Authorization Bearer token`, () => { + return request(appWithoutAuth.getHttpServer()).post("/api/v2/oauth/100/exchange").expect(400); + }); - // Create a test OAuth client - user = await usersFixtures.create({ - email: userEmail, + it(`POST /oauth/:clientId/refresh missing ${X_CAL_SECRET_KEY} header with secret`, () => { + return request(appWithoutAuth.getHttpServer()).post("/api/v2/oauth/100/refresh").expect(401); + }); + + afterAll(async () => { + await appWithoutAuth.close(); }); - organization = await teamRepositoryFixture.create({ name: "organization" }); - membership = await membershipFixtures.addUserToOrg(user, organization, "OWNER", true); - testClient = await createOAuthClient(organization.id); }); - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["redirect-uri"], - permissions: 32, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - describe("Authorize Endpoint", () => { - it.only("POST /oauth/:clientId/authorize", async () => { - const body: OAuthAuthorizeInput = { - redirectUri: testClient.redirectUris[0], - }; + describe("User Authenticated", () => { + let app: INestApplication; + + let usersRepositoryFixtures: UserRepositoryFixture; + let organizationsRepositoryFixture: TeamRepositoryFixture; + let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture; + + let user: User; + let organization: Team; + let oAuthClient: PlatformOAuthClient; + + let authorizationCode: string | null; + let refreshToken: string; - const REDIRECT_STATUS = 302; + beforeAll(async () => { + const userEmail = "developer@platform.com"; - const response = await request(app.getHttpServer()) - .post(`/oauth/${testClient.id}/authorize`) - .send(body) - .expect(REDIRECT_STATUS); + const moduleRef: TestingModule = await withNextAuth( + userEmail, + Test.createTestingModule({ + providers: [PrismaExceptionFilter, HttpExceptionFilter, ZodExceptionFilter], + imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule], + }) + ).compile(); - console.log("asap response", JSON.stringify(response, null, 2)); + app = moduleRef.createNestApplication(); + await app.init(); - const baseUrl = "http://www.localhost/"; - const redirectUri = new URL(response.header.location, baseUrl); - authorizationCode = redirectUri.searchParams.get("code"); - expect(authorizationCode).toBeDefined(); + oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + usersRepositoryFixtures = new UserRepositoryFixture(moduleRef); + + user = await usersRepositoryFixtures.create({ + email: userEmail, + }); + organization = await organizationsRepositoryFixture.create({ name: "organization" }); + oAuthClient = await createOAuthClient(organization.id); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: ["redirect-uri.com"], + permissions: 32, + }; + const secret = "secret"; + + const client = await oAuthClientsRepositoryFixture.create(organizationId, data, secret); + return client; + } + + describe("Authorize Endpoint", () => { + it("POST /oauth/:clientId/authorize", async () => { + const body: OAuthAuthorizeInput = { + redirectUri: oAuthClient.redirectUris[0], + }; + + const REDIRECT_STATUS = 302; + + const response = await request(app.getHttpServer()) + .post(`/oauth/${oAuthClient.id}/authorize`) + .send(body) + .expect(REDIRECT_STATUS); + + const baseUrl = "http://www.localhost/"; + const redirectUri = new URL(response.header.location, baseUrl); + authorizationCode = redirectUri.searchParams.get("code"); + expect(authorizationCode).toBeDefined(); + }); }); - }); - // describe('Exchange Endpoint', () => { - // it('POST /oauth/:clientId/exchange', async () => { - // const authorizationToken = 'Bearer exampleBearerToken'; - // const body = { - // clientSecret: testClient.secret, - // code: authorizationCode, - // }; - - // const response = await request(app.getHttpServer()) - // .post(`/oauth/${testClient.id}/exchange`) - // .set('Authorization', authorizationToken) - // .send(body) - // .expect(200); - - // expect(response.body).toHaveProperty('data'); - // expect(response.body.data).toHaveProperty('accessToken'); - // expect(response.body.data).toHaveProperty('refreshToken'); - // refreshToken = response.body.data.refreshToken; - // }); - // }); - - // describe('Refresh Token Endpoint', () => { - // it('POST /oauth/:clientId/refresh', () => { - // const secretKey = testClient.secret; - // const body = { - // refreshToken, - // }; - - // return request(app.getHttpServer()) - // .post(`/oauth/${testClient.id}/refresh`) - // .set('x-cal-secret-key', secretKey) - // .send(body) - // .expect(200) - // .then((response) => { - // expect(response.body).toHaveProperty('data'); - // expect(response.body.data).toHaveProperty('accessToken'); - // expect(response.body.data).toHaveProperty('refreshToken'); - // }); - // }); - // }); - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(testClient.id); - await teamRepositoryFixture.delete(organization.id); - await usersFixtures.delete(user.id); - - await app.close(); + describe("Exchange Endpoint", () => { + it("POST /oauth/:clientId/exchange", async () => { + const authorizationToken = `Bearer ${authorizationCode}`; + const body: ExchangeAuthorizationCodeInput = { + clientSecret: oAuthClient.secret, + }; + + const response = await request(app.getHttpServer()) + .post(`/oauth/${oAuthClient.id}/exchange`) + .set("Authorization", authorizationToken) + .send(body) + .expect(200); + + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("accessToken"); + expect(response.body.data).toHaveProperty("refreshToken"); + refreshToken = response.body.data.refreshToken; + }); + }); + + describe("Refresh Token Endpoint", () => { + it("POST /oauth/:clientId/refresh", () => { + const secretKey = oAuthClient.secret; + const body = { + refreshToken, + }; + + return request(app.getHttpServer()) + .post(`/oauth/${oAuthClient.id}/refresh`) + .set("x-cal-secret-key", secretKey) + .send(body) + .expect(200) + .then((response) => { + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("accessToken"); + expect(response.body.data).toHaveProperty("refreshToken"); + }); + }); + }); + + afterAll(async () => { + await oAuthClientsRepositoryFixture.delete(oAuthClient.id); + await organizationsRepositoryFixture.delete(organization.id); + await usersRepositoryFixtures.delete(user.id); + + await app.close(); + }); }); }); From b3c248ee4d769febe9701e080152d1824fd8f05c Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 3 Jan 2024 11:31:33 +0200 Subject: [PATCH 11/11] oauth flow tests --- .../oauth-flow/oauth-flow.controller.e2e-spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts index 87c02f14d1a1c3..2081240bbe0c5e 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts @@ -120,6 +120,7 @@ describe("OAuthFlow Endpoints", () => { const baseUrl = "http://www.localhost/"; const redirectUri = new URL(response.header.location, baseUrl); authorizationCode = redirectUri.searchParams.get("code"); + expect(authorizationCode).toBeDefined(); }); }); @@ -137,9 +138,9 @@ describe("OAuthFlow Endpoints", () => { .send(body) .expect(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data).toHaveProperty("accessToken"); - expect(response.body.data).toHaveProperty("refreshToken"); + expect(response.body?.data?.accessToken).toBeDefined(); + expect(response.body?.data?.refreshToken).toBeDefined(); + refreshToken = response.body.data.refreshToken; }); }); @@ -157,9 +158,8 @@ describe("OAuthFlow Endpoints", () => { .send(body) .expect(200) .then((response) => { - expect(response.body).toHaveProperty("data"); - expect(response.body.data).toHaveProperty("accessToken"); - expect(response.body.data).toHaveProperty("refreshToken"); + expect(response.body?.data?.accessToken).toBeDefined(); + expect(response.body?.data?.refreshToken).toBeDefined(); }); }); });