From 604ac83f8be6ff87b1f42c131231838f7c5a619c Mon Sep 17 00:00:00 2001 From: 0x1 <13666360+0x1@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:22:58 -0800 Subject: [PATCH 1/4] remove sign in with world id hasura infrastructure --- hasura/metadata/actions.graphql | 11 ----------- hasura/metadata/actions.yaml | 12 ------------ hasura/metadata/cron_triggers.yaml | 14 -------------- 3 files changed, 37 deletions(-) diff --git a/hasura/metadata/actions.graphql b/hasura/metadata/actions.graphql index 5459b0de4..37e0de030 100644 --- a/hasura/metadata/actions.graphql +++ b/hasura/metadata/actions.graphql @@ -79,13 +79,6 @@ type Mutation { ): ResetAPIOutput } -type Mutation { - reset_client_secret( - app_id: String! - team_id: String! - ): ResetClientOutput -} - type Mutation { rotate_signer_key( app_id: String! @@ -233,10 +226,6 @@ input ChangeAppReportStatusInput { updates: [ChangeAppReportStatusUpdate!]! } -type ResetClientOutput { - client_secret: String! -} - type ResetAPIOutput { api_key: String! } diff --git a/hasura/metadata/actions.yaml b/hasura/metadata/actions.yaml index 9809f4440..1797e15c5 100644 --- a/hasura/metadata/actions.yaml +++ b/hasura/metadata/actions.yaml @@ -148,17 +148,6 @@ actions: permissions: - role: user comment: Reset the given API key for the developer portal - - name: reset_client_secret - definition: - kind: synchronous - handler: '{{NEXT_API_URL}}/hasura/reset-client-secret' - headers: - - name: Authorization - value_from_env: INTERNAL_ENDPOINTS_SECRET - permissions: - - role: api_key - - role: user - comment: Reset the client secret for a Sign in with World ID application - name: rotate_signer_key definition: kind: synchronous @@ -360,7 +349,6 @@ custom_types: - name: ChangeAppReportStatusUpdate - name: ChangeAppReportStatusInput objects: - - name: ResetClientOutput - name: ResetAPIOutput - name: InviteTeamMembersOutput - name: PresignedPost diff --git a/hasura/metadata/cron_triggers.yaml b/hasura/metadata/cron_triggers.yaml index 07132985a..5ea3eb36b 100644 --- a/hasura/metadata/cron_triggers.yaml +++ b/hasura/metadata/cron_triggers.yaml @@ -26,20 +26,6 @@ - name: Authorization value_from_env: INTERNAL_ENDPOINTS_SECRET comment: "" -- name: Delete expired jwks - webhook: '{{NEXT_API_URL}}/_delete-jwks' - schedule: 0 * * * * - include_in_metadata: true - payload: {} - retry_conf: - num_retries: 1 - retry_interval_seconds: 10 - timeout_seconds: 60 - tolerance_seconds: 21600 - headers: - - name: Authorization - value_from_env: INTERNAL_ENDPOINTS_SECRET - comment: Schedules all expired JWKS for deletion by KMS - name: Rollup app stats webhook: '{{NEXT_API_URL}}/_rollup-app-stats' schedule: 0 * * * * From 98378b698360948aa79bc49dcd89264ac2fd7f9e Mon Sep 17 00:00:00 2001 From: 0x1 <13666360+0x1@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:23:08 -0800 Subject: [PATCH 2/4] remove sign in with world id API and UI --- .../auxiliary/auxiliary-endpoints.spec.ts | 21 - web/.env.example | 3 - web/api/_delete-jwks/index.ts | 21 - .../graphql/get-membership.generated.ts | 71 -- .../graphql/get-membership.graphql | 11 - .../graphql/update-secret.generated.ts | 68 -- .../graphql/update-secret.graphql | 8 - web/api/hasura/reset-client-secret/index.ts | 104 --- .../graphql/delete-expired-jwks.generated.ts | 71 -- .../jwks/graphql/delete-expired-jwks.gql | 8 - ...tive-jwks-by-expiration-query.generated.ts | 71 -- .../fetch-active-jwks-by-expiration-query.gql | 10 - .../jwks/graphql/insert-jwk.generated.ts | 79 -- web/api/helpers/jwks/graphql/insert-jwk.gql | 17 - .../jwks/graphql/retrieve-jwk.generated.ts | 67 -- web/api/helpers/jwks/graphql/retrieve-jwk.gql | 7 - web/api/helpers/jwks/index.ts | 204 ----- web/api/helpers/jwts.ts | 110 --- web/api/helpers/kms.ts | 42 - .../fetch-app-secret-query.generated.ts | 75 -- .../oidc/graphql/fetch-app-secret-query.gql | 15 - .../oidc/graphql/fetch-oidc-app.generated.ts | 88 -- .../helpers/oidc/graphql/fetch-oidc-app.gql | 21 - .../graphql/insert-auth-code.generated.ts | 99 --- .../helpers/oidc/graphql/insert-auth-code.gql | 30 - web/api/helpers/oidc/index.ts | 240 ------ web/api/v1/jwks/graphql/get-jwks.generated.ts | 65 -- web/api/v1/jwks/graphql/get-jwks.graphql | 7 - web/api/v1/jwks/index.ts | 28 - .../graphql/fetch-nullifier.generated.ts | 60 -- .../authorize/graphql/fetch-nullifier.graphql | 5 - .../graphql/upsert-nullifier.generated.ts | 70 -- .../graphql/upsert-nullifier.graphql | 9 - web/api/v1/oidc/authorize/index.ts | 439 ---------- web/api/v1/oidc/introspect/index.ts | 75 -- web/api/v1/oidc/openid-configuration/index.ts | 38 - .../graphql/delete-auth-code.generated.ts | 95 --- .../token/graphql/delete-auth-code.graphql | 24 - .../graphql/fetch-redirect-count.generated.ts | 61 -- .../graphql/fetch-redirect-count.graphql | 5 - web/api/v1/oidc/token/index.ts | 244 ------ web/api/v1/oidc/userinfo/index.ts | 109 --- web/api/v1/oidc/validate/index.ts | 69 -- .../[appId]/sign-in-with-world-id/layout.tsx | 2 - .../[appId]/sign-in-with-world-id/page.tsx | 9 - .../proof-debugging/page.tsx | 2 - web/app/api/%5Fdelete-jwks/route.ts | 1 - .../api/hasura/reset-client-secret/route.ts | 1 - web/app/api/v1/jwks/route.ts | 1 - web/app/api/v1/oidc/authorize/route.ts | 1 - web/app/api/v1/oidc/introspect/route.ts | 1 - .../api/v1/oidc/openid-configuration/route.ts | 1 - web/app/api/v1/oidc/token/route.ts | 1 - web/app/api/v1/oidc/userinfo/route.ts | 1 - web/app/api/v1/oidc/validate/route.ts | 1 - web/lib/constants.ts | 6 - web/lib/urls.ts | 3 - web/next.config.mjs | 4 - .../ProofDebugging/page/index.tsx | 7 - .../AppId/SignInWithWorldId/error/error.tsx | 22 - .../AppId/SignInWithWorldId/layout/index.tsx | 10 - .../page/ClientInformation/Links/index.tsx | 101 --- .../Redirects/RedirectInput/index.tsx | 121 --- .../client/delete-redirect.generated.ts | 65 -- .../graphql/client/delete-redirect.graphql | 5 - .../client/fetch-redirect.generated.ts | 106 --- .../graphql/client/fetch-redirect.graphql | 12 - .../client/insert-redirect.generated.ts | 78 -- .../graphql/client/insert-redirect.graphql | 9 - .../client/update-redirect.generated.ts | 81 -- .../graphql/client/update-redirect.graphql | 9 - .../ClientInformation/Redirects/index.tsx | 181 ----- .../client/fetch-sign-in-action.generated.ts | 113 --- .../client/fetch-sign-in-action.graphql | 13 - .../graphql/client/reset-secret.generated.ts | 70 -- .../graphql/client/reset-secret.graphql | 5 - .../client/update-sign-in-action.generated.ts | 67 -- .../client/update-sign-in-action.graphql | 5 - .../page/ClientInformation/index.tsx | 198 ----- .../graphql/server/fetch-signin.generated.ts | 71 -- .../page/graphql/server/fetch-signin.graphql | 9 - .../AppId/SignInWithWorldId/page/index.tsx | 104 --- .../TeamId/Apps/AppId/layout/AppIdChrome.tsx | 20 - .../Members/List/PermissionsDialog/index.tsx | 10 - web/tests/api/__mocks__/jwks.mock.ts | 10 - web/tests/api/__mocks__/kms.mock.ts | 1 - web/tests/api/delete-jwks.test.ts | 112 --- web/tests/api/v1/oidc/authorize.test.ts | 375 --------- web/tests/api/v1/oidc/userinfo.test.ts | 46 -- web/tests/api/v1/oidc/validate.test.ts | 108 --- web/tests/integration/action.test.ts | 7 - web/tests/integration/app.test.ts | 128 --- web/tests/integration/jwks.test.ts | 92 --- web/tests/integration/oidc/authorize.test.ts | 151 ---- web/tests/integration/oidc/token.test.ts | 749 ------------------ web/tests/integration/team.test.ts | 6 +- web/tests/unit/check-flow-type.test.ts | 59 -- 97 files changed, 3 insertions(+), 6332 deletions(-) delete mode 100644 web/api/_delete-jwks/index.ts delete mode 100644 web/api/hasura/reset-client-secret/graphql/get-membership.generated.ts delete mode 100644 web/api/hasura/reset-client-secret/graphql/get-membership.graphql delete mode 100644 web/api/hasura/reset-client-secret/graphql/update-secret.generated.ts delete mode 100644 web/api/hasura/reset-client-secret/graphql/update-secret.graphql delete mode 100644 web/api/hasura/reset-client-secret/index.ts delete mode 100644 web/api/helpers/jwks/graphql/delete-expired-jwks.generated.ts delete mode 100644 web/api/helpers/jwks/graphql/delete-expired-jwks.gql delete mode 100644 web/api/helpers/jwks/graphql/fetch-active-jwks-by-expiration-query.generated.ts delete mode 100644 web/api/helpers/jwks/graphql/fetch-active-jwks-by-expiration-query.gql delete mode 100644 web/api/helpers/jwks/graphql/insert-jwk.generated.ts delete mode 100644 web/api/helpers/jwks/graphql/insert-jwk.gql delete mode 100644 web/api/helpers/jwks/graphql/retrieve-jwk.generated.ts delete mode 100644 web/api/helpers/jwks/graphql/retrieve-jwk.gql delete mode 100644 web/api/helpers/jwks/index.ts delete mode 100644 web/api/helpers/oidc/graphql/fetch-app-secret-query.generated.ts delete mode 100644 web/api/helpers/oidc/graphql/fetch-app-secret-query.gql delete mode 100644 web/api/helpers/oidc/graphql/fetch-oidc-app.generated.ts delete mode 100644 web/api/helpers/oidc/graphql/fetch-oidc-app.gql delete mode 100644 web/api/helpers/oidc/graphql/insert-auth-code.generated.ts delete mode 100644 web/api/helpers/oidc/graphql/insert-auth-code.gql delete mode 100644 web/api/helpers/oidc/index.ts delete mode 100644 web/api/v1/jwks/graphql/get-jwks.generated.ts delete mode 100644 web/api/v1/jwks/graphql/get-jwks.graphql delete mode 100644 web/api/v1/jwks/index.ts delete mode 100644 web/api/v1/oidc/authorize/graphql/fetch-nullifier.generated.ts delete mode 100644 web/api/v1/oidc/authorize/graphql/fetch-nullifier.graphql delete mode 100644 web/api/v1/oidc/authorize/graphql/upsert-nullifier.generated.ts delete mode 100644 web/api/v1/oidc/authorize/graphql/upsert-nullifier.graphql delete mode 100644 web/api/v1/oidc/authorize/index.ts delete mode 100644 web/api/v1/oidc/introspect/index.ts delete mode 100644 web/api/v1/oidc/openid-configuration/index.ts delete mode 100644 web/api/v1/oidc/token/graphql/delete-auth-code.generated.ts delete mode 100644 web/api/v1/oidc/token/graphql/delete-auth-code.graphql delete mode 100644 web/api/v1/oidc/token/graphql/fetch-redirect-count.generated.ts delete mode 100644 web/api/v1/oidc/token/graphql/fetch-redirect-count.graphql delete mode 100644 web/api/v1/oidc/token/index.ts delete mode 100644 web/api/v1/oidc/userinfo/index.ts delete mode 100644 web/api/v1/oidc/validate/index.ts delete mode 100644 web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/layout.tsx delete mode 100644 web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/page.tsx delete mode 100644 web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/proof-debugging/page.tsx delete mode 100644 web/app/api/%5Fdelete-jwks/route.ts delete mode 100644 web/app/api/hasura/reset-client-secret/route.ts delete mode 100644 web/app/api/v1/jwks/route.ts delete mode 100644 web/app/api/v1/oidc/authorize/route.ts delete mode 100644 web/app/api/v1/oidc/introspect/route.ts delete mode 100644 web/app/api/v1/oidc/openid-configuration/route.ts delete mode 100644 web/app/api/v1/oidc/token/route.ts delete mode 100644 web/app/api/v1/oidc/userinfo/route.ts delete mode 100644 web/app/api/v1/oidc/validate/route.ts delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/ProofDebugging/page/index.tsx delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/error/error.tsx delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/layout/index.tsx delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Links/index.tsx delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/RedirectInput/index.tsx delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/delete-redirect.generated.ts delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/delete-redirect.graphql delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/fetch-redirect.generated.ts delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/fetch-redirect.graphql delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/insert-redirect.generated.ts delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/insert-redirect.graphql delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/update-redirect.generated.ts delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/update-redirect.graphql delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/index.tsx delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/fetch-sign-in-action.generated.ts delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/fetch-sign-in-action.graphql delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/reset-secret.generated.ts delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/reset-secret.graphql delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/update-sign-in-action.generated.ts delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/update-sign-in-action.graphql delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/index.tsx delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/graphql/server/fetch-signin.generated.ts delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/graphql/server/fetch-signin.graphql delete mode 100644 web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/index.tsx delete mode 100644 web/tests/api/__mocks__/jwks.mock.ts delete mode 100644 web/tests/api/delete-jwks.test.ts delete mode 100644 web/tests/api/v1/oidc/authorize.test.ts delete mode 100644 web/tests/api/v1/oidc/userinfo.test.ts delete mode 100644 web/tests/api/v1/oidc/validate.test.ts delete mode 100644 web/tests/integration/jwks.test.ts delete mode 100644 web/tests/integration/oidc/authorize.test.ts delete mode 100644 web/tests/integration/oidc/token.test.ts delete mode 100644 web/tests/unit/check-flow-type.test.ts diff --git a/tests/api/specs/auxiliary/auxiliary-endpoints.spec.ts b/tests/api/specs/auxiliary/auxiliary-endpoints.spec.ts index b92da7fb0..73e457410 100644 --- a/tests/api/specs/auxiliary/auxiliary-endpoints.spec.ts +++ b/tests/api/specs/auxiliary/auxiliary-endpoints.spec.ts @@ -47,27 +47,6 @@ describe("Auxiliary API Endpoints", () => { }); }); - describe("POST /api/_delete-jwks", () => { - it("Delete Expired JWKs With Authorization", async () => { - const response = await axios.post( - `${INTERNAL_API_URL}/api/_delete-jwks`, - {}, - { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - }, - ); - - expect( - response.status, - `Delete expired JWKs request resolved with a wrong code:\n${JSON.stringify(response.data, null, 2)}`, - ).toBe(204); // This endpoint returns 204 No Content - expect(response.data).toBe(""); // Empty response body - }); - }); - describe("POST /api/_gen-external-nullifier", () => { let testTeamId: string | undefined; let testAppId: string | undefined; diff --git a/web/.env.example b/web/.env.example index 648faaedf..2372ac8e2 100644 --- a/web/.env.example +++ b/web/.env.example @@ -40,9 +40,6 @@ SENDGRID_API_KEY=your_sendgrid_api_key SENDGRID_EMAIL_FROM=your_email@example.com SENDGRID_TEAM_INVITE_TEMPLATE_ID=d-5cbc349e16344604813f74c1f8b0bbdc -# Sign in with World ID -SIGN_IN_WITH_WORLD_ID_APP_ID= - # Ironclad IRONCLAD_ACCESS_ID=your_ironclad_access_id IRONCLAD_GROUP_KEY=your_ironclad_group_key diff --git a/web/api/_delete-jwks/index.ts b/web/api/_delete-jwks/index.ts deleted file mode 100644 index db0cba148..000000000 --- a/web/api/_delete-jwks/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { _deleteExpiredJWKs } from "@/api/helpers/jwks"; -import { protectInternalEndpoint } from "@/api/helpers/utils"; -import { logger } from "@/lib/logger"; -import { NextRequest, NextResponse } from "next/server"; - -/** - * Deletes expired JWKs - */ -export async function POST(request: NextRequest) { - const { isAuthenticated, errorResponse } = protectInternalEndpoint(request); - if (!isAuthenticated) { - return errorResponse; - } - logger.info("Starting deletion of expired jwks."); - - const response = await _deleteExpiredJWKs(); - - logger.info(`Deleted ${response} expired jwks.`); - - return new NextResponse(null, { status: 204 }); -} diff --git a/web/api/hasura/reset-client-secret/graphql/get-membership.generated.ts b/web/api/hasura/reset-client-secret/graphql/get-membership.generated.ts deleted file mode 100644 index e24bba16f..000000000 --- a/web/api/hasura/reset-client-secret/graphql/get-membership.generated.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type GetMembershipQueryVariables = Types.Exact<{ - team_id: Types.Scalars["String"]["input"]; - user_id: Types.Scalars["String"]["input"]; - app_id: Types.Scalars["String"]["input"]; -}>; - -export type GetMembershipQuery = { - __typename?: "query_root"; - team: Array<{ __typename?: "team"; id: string }>; -}; - -export const GetMembershipDocument = gql` - query GetMembership($team_id: String!, $user_id: String!, $app_id: String!) { - team( - where: { - id: { _eq: $team_id } - memberships: { - user_id: { _eq: $user_id } - role: { _in: [ADMIN, OWNER] } - } - apps: { id: { _eq: $app_id } } - } - ) { - id - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - GetMembership( - variables: GetMembershipQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request(GetMembershipDocument, variables, { - ...requestHeaders, - ...wrappedRequestHeaders, - }), - "GetMembership", - "query", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/hasura/reset-client-secret/graphql/get-membership.graphql b/web/api/hasura/reset-client-secret/graphql/get-membership.graphql deleted file mode 100644 index bd46dc78f..000000000 --- a/web/api/hasura/reset-client-secret/graphql/get-membership.graphql +++ /dev/null @@ -1,11 +0,0 @@ -query GetMembership($team_id: String!, $user_id: String!, $app_id: String!) { - team( - where: { - id: { _eq: $team_id } - memberships: { user_id: { _eq: $user_id }, role: { _in: [ADMIN, OWNER] } } - apps: { id: { _eq: $app_id } } - } - ) { - id - } -} diff --git a/web/api/hasura/reset-client-secret/graphql/update-secret.generated.ts b/web/api/hasura/reset-client-secret/graphql/update-secret.generated.ts deleted file mode 100644 index c6bf48487..000000000 --- a/web/api/hasura/reset-client-secret/graphql/update-secret.generated.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type UpdateSecretMutationVariables = Types.Exact<{ - app_id: Types.Scalars["String"]["input"]; - hashed_secret: Types.Scalars["String"]["input"]; -}>; - -export type UpdateSecretMutation = { - __typename?: "mutation_root"; - update_action?: { - __typename?: "action_mutation_response"; - affected_rows: number; - } | null; -}; - -export const UpdateSecretDocument = gql` - mutation UpdateSecret($app_id: String!, $hashed_secret: String!) { - update_action( - where: { app_id: { _eq: $app_id }, action: { _eq: "" } } - _set: { client_secret: $hashed_secret } - ) { - affected_rows - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - UpdateSecret( - variables: UpdateSecretMutationVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request( - UpdateSecretDocument, - variables, - { ...requestHeaders, ...wrappedRequestHeaders }, - ), - "UpdateSecret", - "mutation", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/hasura/reset-client-secret/graphql/update-secret.graphql b/web/api/hasura/reset-client-secret/graphql/update-secret.graphql deleted file mode 100644 index 2e27c5c13..000000000 --- a/web/api/hasura/reset-client-secret/graphql/update-secret.graphql +++ /dev/null @@ -1,8 +0,0 @@ -mutation UpdateSecret($app_id: String!, $hashed_secret: String!) { - update_action( - where: { app_id: { _eq: $app_id }, action: { _eq: "" } } - _set: { client_secret: $hashed_secret } - ) { - affected_rows - } -} diff --git a/web/api/hasura/reset-client-secret/index.ts b/web/api/hasura/reset-client-secret/index.ts deleted file mode 100644 index 12caab52b..000000000 --- a/web/api/hasura/reset-client-secret/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { errorHasuraQuery } from "@/api/helpers/errors"; -import { getAPIServiceGraphqlClient } from "@/api/helpers/graphql"; -import { - generateHashedSecret, - protectInternalEndpoint, -} from "@/api/helpers/utils"; -import { logger } from "@/lib/logger"; -import { NextRequest, NextResponse } from "next/server"; -import { getSdk as getMembershipSdk } from "./graphql/get-membership.generated"; -import { getSdk as updateSecretSDK } from "./graphql/update-secret.generated"; - -export const POST = async (req: NextRequest) => { - const { isAuthenticated, errorResponse } = protectInternalEndpoint(req); - if (!isAuthenticated) { - return errorResponse; - } - const body = await req.json(); - - if (body?.action.name !== "reset_client_secret") { - return errorHasuraQuery({ - req, - detail: "Invalid action.", - code: "invalid_action", - }); - } - - if (body.session_variables["x-hasura-role"] === "admin") { - logger.error("Admin not allowed to run _reset-client-client-secret"), - { role: body.session_variables["x-hasura-role"] }; - return errorHasuraQuery({ - req, - detail: "Admin is not allowed to run this query.", - code: "admin_not_allowed", - }); - } - - const userId = body.session_variables["x-hasura-user-id"]; - if (!userId) { - return errorHasuraQuery({ - req, - detail: "userId must be set.", - code: "required", - }); - } - - const team_id = body.input.team_id; - if (!team_id) { - return errorHasuraQuery({ - req, - detail: "team_id must be set.", - code: "required", - }); - } - - const app_id = body.input.app_id; - if (!app_id) { - return errorHasuraQuery({ - req, - detail: "`app_id` is a required input.", - code: "required", - team_id, - }); - } - - const client = await getAPIServiceGraphqlClient(); - - // ANCHOR: Make sure the user can perform this client reset - const { team: teamMembershipQuery } = await getMembershipSdk( - client, - ).GetMembership({ - user_id: userId, - team_id, - app_id, - }); - - if (!teamMembershipQuery || !teamMembershipQuery.length) { - return errorHasuraQuery({ - req, - detail: "Insufficient Permissions", - code: "insufficient_permissions", - team_id, - app_id, - }); - } - - const { secret: client_secret, hashed_secret } = generateHashedSecret(app_id); - const { update_action: updateResponse } = await updateSecretSDK( - client, - ).UpdateSecret({ - app_id: app_id, - hashed_secret, - }); - - if (!updateResponse?.affected_rows) { - return errorHasuraQuery({ - req, - detail: "Failed to reset the client secret.", - code: "update_failed", - team_id, - app_id, - }); - } - return NextResponse.json({ client_secret }); -}; diff --git a/web/api/helpers/jwks/graphql/delete-expired-jwks.generated.ts b/web/api/helpers/jwks/graphql/delete-expired-jwks.generated.ts deleted file mode 100644 index ca00f02b8..000000000 --- a/web/api/helpers/jwks/graphql/delete-expired-jwks.generated.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type DeleteExpiredJwKsMutationVariables = Types.Exact<{ - expired_by?: Types.InputMaybe; -}>; - -export type DeleteExpiredJwKsMutation = { - __typename?: "mutation_root"; - delete_jwks?: { - __typename?: "jwks_mutation_response"; - returning: Array<{ - __typename?: "jwks"; - id: string; - kms_id?: string | null; - }>; - } | null; -}; - -export const DeleteExpiredJwKsDocument = gql` - mutation DeleteExpiredJWKs($expired_by: timestamptz = "") { - delete_jwks(where: { expires_at: { _lte: $expired_by } }) { - returning { - id - kms_id - } - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - DeleteExpiredJWKs( - variables?: DeleteExpiredJwKsMutationVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request( - DeleteExpiredJwKsDocument, - variables, - { ...requestHeaders, ...wrappedRequestHeaders }, - ), - "DeleteExpiredJWKs", - "mutation", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/helpers/jwks/graphql/delete-expired-jwks.gql b/web/api/helpers/jwks/graphql/delete-expired-jwks.gql deleted file mode 100644 index e9a76de7c..000000000 --- a/web/api/helpers/jwks/graphql/delete-expired-jwks.gql +++ /dev/null @@ -1,8 +0,0 @@ -mutation DeleteExpiredJWKs($expired_by: timestamptz = "") { - delete_jwks(where: { expires_at: { _lte: $expired_by } }) { - returning { - id - kms_id - } - } -} diff --git a/web/api/helpers/jwks/graphql/fetch-active-jwks-by-expiration-query.generated.ts b/web/api/helpers/jwks/graphql/fetch-active-jwks-by-expiration-query.generated.ts deleted file mode 100644 index 921bbbb37..000000000 --- a/web/api/helpers/jwks/graphql/fetch-active-jwks-by-expiration-query.generated.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type FetchActiveJwKsByExpirationQueryVariables = Types.Exact<{ - expires_at: Types.Scalars["timestamptz"]["input"]; -}>; - -export type FetchActiveJwKsByExpirationQuery = { - __typename?: "query_root"; - jwks: Array<{ - __typename?: "jwks"; - id: string; - kms_id?: string | null; - expires_at: string; - }>; -}; - -export const FetchActiveJwKsByExpirationDocument = gql` - query FetchActiveJWKsByExpiration($expires_at: timestamptz!) { - jwks( - where: { expires_at: { _gt: $expires_at } } - order_by: { expires_at: desc } - ) { - id - kms_id - expires_at - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - FetchActiveJWKsByExpiration( - variables: FetchActiveJwKsByExpirationQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request( - FetchActiveJwKsByExpirationDocument, - variables, - { ...requestHeaders, ...wrappedRequestHeaders }, - ), - "FetchActiveJWKsByExpiration", - "query", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/helpers/jwks/graphql/fetch-active-jwks-by-expiration-query.gql b/web/api/helpers/jwks/graphql/fetch-active-jwks-by-expiration-query.gql deleted file mode 100644 index 0df06c622..000000000 --- a/web/api/helpers/jwks/graphql/fetch-active-jwks-by-expiration-query.gql +++ /dev/null @@ -1,10 +0,0 @@ -query FetchActiveJWKsByExpiration($expires_at: timestamptz!) { - jwks( - where: { expires_at: { _gt: $expires_at } } - order_by: { expires_at: desc } - ) { - id - kms_id - expires_at - } -} diff --git a/web/api/helpers/jwks/graphql/insert-jwk.generated.ts b/web/api/helpers/jwks/graphql/insert-jwk.generated.ts deleted file mode 100644 index 57904ae79..000000000 --- a/web/api/helpers/jwks/graphql/insert-jwk.generated.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type InsertJwkMutationVariables = Types.Exact<{ - expires_at: Types.Scalars["timestamptz"]["input"]; - public_jwk: Types.Scalars["jsonb"]["input"]; - kms_id: Types.Scalars["String"]["input"]; -}>; - -export type InsertJwkMutation = { - __typename?: "mutation_root"; - insert_jwks_one?: { - __typename?: "jwks"; - id: string; - kms_id?: string | null; - expires_at: string; - } | null; -}; - -export const InsertJwkDocument = gql` - mutation InsertJWK( - $expires_at: timestamptz! - $public_jwk: jsonb! - $kms_id: String! - ) { - insert_jwks_one( - object: { - expires_at: $expires_at - kms_id: $kms_id - public_jwk: $public_jwk - } - ) { - id - kms_id - expires_at - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - InsertJWK( - variables: InsertJwkMutationVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request(InsertJwkDocument, variables, { - ...requestHeaders, - ...wrappedRequestHeaders, - }), - "InsertJWK", - "mutation", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/helpers/jwks/graphql/insert-jwk.gql b/web/api/helpers/jwks/graphql/insert-jwk.gql deleted file mode 100644 index 3edab5578..000000000 --- a/web/api/helpers/jwks/graphql/insert-jwk.gql +++ /dev/null @@ -1,17 +0,0 @@ -mutation InsertJWK( - $expires_at: timestamptz! - $public_jwk: jsonb! - $kms_id: String! -) { - insert_jwks_one( - object: { - expires_at: $expires_at - kms_id: $kms_id - public_jwk: $public_jwk - } - ) { - id - kms_id - expires_at - } -} diff --git a/web/api/helpers/jwks/graphql/retrieve-jwk.generated.ts b/web/api/helpers/jwks/graphql/retrieve-jwk.generated.ts deleted file mode 100644 index 5c3474f1b..000000000 --- a/web/api/helpers/jwks/graphql/retrieve-jwk.generated.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type RetrieveJwkQueryVariables = Types.Exact<{ - kid: Types.Scalars["String"]["input"]; -}>; - -export type RetrieveJwkQuery = { - __typename?: "query_root"; - jwks: Array<{ - __typename?: "jwks"; - id: string; - kms_id?: string | null; - public_jwk: any; - }>; -}; - -export const RetrieveJwkDocument = gql` - query RetrieveJWK($kid: String!) { - jwks(limit: 1, where: { id: { _eq: $kid } }) { - id - kms_id - public_jwk - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - RetrieveJWK( - variables: RetrieveJwkQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request(RetrieveJwkDocument, variables, { - ...requestHeaders, - ...wrappedRequestHeaders, - }), - "RetrieveJWK", - "query", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/helpers/jwks/graphql/retrieve-jwk.gql b/web/api/helpers/jwks/graphql/retrieve-jwk.gql deleted file mode 100644 index a2e4b7e4f..000000000 --- a/web/api/helpers/jwks/graphql/retrieve-jwk.gql +++ /dev/null @@ -1,7 +0,0 @@ -query RetrieveJWK($kid: String!) { - jwks(limit: 1, where: { id: { _eq: $kid } }) { - id - kms_id - public_jwk - } -} diff --git a/web/api/helpers/jwks/index.ts b/web/api/helpers/jwks/index.ts deleted file mode 100644 index 94a4ffc84..000000000 --- a/web/api/helpers/jwks/index.ts +++ /dev/null @@ -1,204 +0,0 @@ -import "server-only"; - -import { getAPIServiceGraphqlClient } from "@/api/helpers/graphql"; -import { - createKMSKey, - getKMSClient, - scheduleKeyDeletion, -} from "@/api/helpers/kms"; -import { JWK_TIME_TO_LIVE, JWK_TTL_USABLE } from "@/lib/constants"; -import { logger } from "@/lib/logger"; -import { createPublicKey } from "crypto"; -import dayjs from "dayjs"; - -import { - getSdk as getRetrieveJWKSdk, - RetrieveJwkQuery, -} from "./graphql/retrieve-jwk.generated"; - -import { - FetchActiveJwKsByExpirationQuery, - getSdk as fetchActiveJWKsByExpirationSdk, -} from "./graphql/fetch-active-jwks-by-expiration-query.generated"; - -import { - InsertJwkMutation, - getSdk as insertJWKSdk, -} from "./graphql/insert-jwk.generated"; - -import { - DeleteExpiredJwKsMutation, - getSdk as deleteExpiredJWKsSdk, -} from "./graphql/delete-expired-jwks.generated"; - -export type CreateJWKResult = { - keyId: string; - publicJwk: JsonWebKey; - createdAt: Date; -}; -/** - * Get the public JWK for a given kid - * @param kid - * @returns - */ -export const retrieveJWK = async (kid: string) => { - const client = await getAPIServiceGraphqlClient(); - - let jwks: RetrieveJwkQuery["jwks"] | null = null; - - try { - const data = await getRetrieveJWKSdk(client).RetrieveJWK({ - kid, - }); - - jwks = data.jwks; - } catch (error) { - logger.error("Error retrieving JWK.", { error }); - - throw error; - } - - if (!jwks?.length) { - throw new Error("JWK not found."); - } - - const { id, kms_id, public_jwk } = jwks[0]; - return { kid: id, kms_id, public_jwk }; -}; - -/** - * Generates an RS256 asymmetric key pair in JWK format - * @returns - */ -export const generateJWK = async (): Promise => { - const client = await getKMSClient(); - - if (client) { - const result = await createKMSKey(client, "RSA_2048"); - if (result?.keyId && result?.publicKey) { - const publicJwk = createPublicKey(result.publicKey).export({ - format: "jwk", - }); - - return { keyId: result.keyId, publicJwk, createdAt: result.createdAt }; - } else { - throw new Error("Unable to create KMS key."); - } - } else { - throw new Error("KMS client not found."); - } -}; - -/** - * Generate new JWK. Generates a new KMS key and stores the public key in the database. - * @param alg - * @returns - */ -export const createAndStoreJWK = async () => { - const key = await generateJWK(); - const expiresAt = dayjs(key.createdAt).add(JWK_TIME_TO_LIVE, "day"); - - const client = await getAPIServiceGraphqlClient(); - - let insertResult: InsertJwkMutation["insert_jwks_one"] | null = null; - - try { - const { insert_jwks_one } = await insertJWKSdk(client).InsertJWK({ - expires_at: expiresAt.toISOString(), - kms_id: key.keyId, - public_jwk: key.publicJwk, - }); - - insertResult = insert_jwks_one; - } catch (error) { - logger.error("Error inserting JWK.", { error }); - throw error; - } - - if (insertResult) { - return insertResult; - } - - logger.error("Unable to create new JWK.", { insertResult }); - throw new Error("Unable to create new JWK."); -}; - -/** - * Fetches an active JWK to sign requests, and otherwise rotates the key - * @param alg - * @returns - */ -export const fetchActiveJWK = async () => { - const apiClient = await getAPIServiceGraphqlClient(); - - let jwks: FetchActiveJwKsByExpirationQuery["jwks"] | null = null; - - try { - const data = await fetchActiveJWKsByExpirationSdk( - apiClient, - ).FetchActiveJWKsByExpiration({ - expires_at: new Date().toISOString(), - }); - - jwks = data.jwks; - } catch (error) { - logger.error("Error fetching active JWK.", { error }); - throw error; - } - - // JWK is still active - if (jwks?.length) { - const { id, kms_id, expires_at } = jwks[0]; - - // Only return JWK if it's not expiring in the next few days - const now = dayjs(); - const expires = dayjs(expires_at); - if (expires.diff(now, "day") > JWK_TTL_USABLE) { - return { kid: id, kms_id }; - } - } - - // JWK is expired or expiring soon, rotate the key - const jwk = await createAndStoreJWK(); - return { kid: jwk.id, kms_id: jwk.kms_id }; -}; - -/** - * Delete all expired JWKs from the database - * @returns - */ -export const _deleteExpiredJWKs = async () => { - const apiClient = await getAPIServiceGraphqlClient(); - - let deleteResult: DeleteExpiredJwKsMutation["delete_jwks"] | null = null; - - try { - const data = await deleteExpiredJWKsSdk(apiClient).DeleteExpiredJWKs({ - expired_by: new Date(Date.now() - 20 * 60 * 1000).toISOString(), // 20 minutes ago - }); - - deleteResult = data.delete_jwks; - } catch (error) { - logger.error("Error deleting expired JWKs.", { error }); - throw error; - } - - if (deleteResult?.returning) { - // Schedule each KMS key for deletion - const kmsClient = await getKMSClient(); - - if (kmsClient) { - for (const key of deleteResult.returning) { - if (!key.kms_id) { - logger.error("KMS ID not found for JWK.", { key }); - - continue; - } - - await scheduleKeyDeletion(kmsClient, key.kms_id); - } - } - - return deleteResult.returning.length; - } -}; diff --git a/web/api/helpers/jwts.ts b/web/api/helpers/jwts.ts index d8adf775e..57b6fdc41 100644 --- a/web/api/helpers/jwts.ts +++ b/web/api/helpers/jwts.ts @@ -2,15 +2,9 @@ import "server-only"; /** * Contains all backend utilities related to JWTs. - * * OIDC tokens * * Hasura authentication * * Developer Portal authentication */ -import { retrieveJWK } from "@/api/helpers/jwks"; -import { getKMSClient, signJWTWithKMSKey } from "@/api/helpers/kms"; -import { OIDCScopes } from "@/api/helpers/oidc"; -import { VerificationLevel } from "@worldcoin/idkit-core"; -import { randomUUID } from "crypto"; import dayjs from "dayjs"; import * as jose from "jose"; @@ -211,107 +205,3 @@ export const verifySignUpJWT = async (token: string) => { } return { sub }; }; - -// ANCHOR: -----------------OIDC JWTs-------------------------- - -const formatOIDCDateTime = (date: Date | dayjs.Dayjs): number => { - return dayjs(date).unix(); -}; - -interface IVerificationJWT { - kid: string; - kms_id: string; - nonce?: string; - nullifier_hash: string; - app_id: string; - verification_level: VerificationLevel; - scope: OIDCScopes[]; -} - -/** - * Generates a JWT that can be used to verify a proof (used for Sign in with World ID) - * @returns - */ -export const generateOIDCJWT = async ({ - app_id, - nonce, - nullifier_hash, - kid, - verification_level, - scope, -}: IVerificationJWT): Promise => { - const payload = { - iss: JWT_ISSUER, - sub: nullifier_hash, - jti: randomUUID(), - iat: formatOIDCDateTime(new Date()), - exp: formatOIDCDateTime(dayjs().add(1, "hour")), - aud: app_id, - scope: scope.join(" "), - // NOTE: DEPRECATED, will be removed in future versions - "https://id.worldcoin.org/beta": { - likely_human: - verification_level === VerificationLevel.Orb ? "strong" : "weak", - credential_type: verification_level, - warning: - "DEPRECATED and will be removed soon. Use `https://id.worldcoin.org/v1` instead.", - }, - "https://id.worldcoin.org/v1": { - verification_level, - }, - } as Record; - - if (nonce) { - payload.nonce = nonce; - } - - if (scope.includes(OIDCScopes.Email)) { - payload.email = `${nullifier_hash}@id.worldcoin.org`; - } - - if (scope.includes(OIDCScopes.Profile)) { - payload.name = "World ID User"; - payload.given_name = "World ID"; - payload.family_name = "User"; - } - - // Sign the JWT with a KMS managed key - const client = await getKMSClient(); - const header = { - alg: "RS256", - typ: "JWT", - kid, - }; - - if (client) { - const token = await signJWTWithKMSKey(client, header, payload); - if (token) return token; - } - throw new Error("Failed to sign JWT from KMS."); -}; - -export const verifyOIDCJWT = async ( - token: string, -): Promise => { - const { kid } = jose.decodeProtectedHeader(token); - - if (!kid) { - throw new Error("JWT is invalid. Does not contain a `kid` claim."); - } - - const { public_jwk } = await retrieveJWK(kid); - - if (!public_jwk) { - throw new Error("Key for this JWT is invalid."); - } - - const { payload } = await jose.jwtVerify( - token, - await jose.importJWK(public_jwk, "RS256"), - { - issuer: JWT_ISSUER, - }, - ); - - return payload; -}; diff --git a/web/api/helpers/kms.ts b/web/api/helpers/kms.ts index 75a1ee7c6..7779c6c41 100644 --- a/web/api/helpers/kms.ts +++ b/web/api/helpers/kms.ts @@ -4,7 +4,6 @@ import "server-only"; * Contains all functions for interacting with Amazon KMS */ -import { retrieveJWK } from "@/api/helpers/jwks"; import { logger } from "@/lib/logger"; import { CreateKeyCommand, @@ -13,9 +12,7 @@ import { KMSClient, KeySpec, ScheduleKeyDeletionCommand, - SignCommand, } from "@aws-sdk/client-kms"; -import { base64url } from "jose"; export type CreateKeyResult = | { @@ -79,45 +76,6 @@ export const getKMSKeyStatus = async (client: KMSClient, keyId: string) => { } }; -export const signJWTWithKMSKey = async ( - client: KMSClient, - header: Record, - payload: Record, -) => { - const encodedHeader = base64url.encode(JSON.stringify(header)); - const encodedPayload = base64url.encode(JSON.stringify(payload)); - const encodedHeaderPayload = `${encodedHeader}.${encodedPayload}`; - - try { - const { kms_id } = await retrieveJWK(header.kid); // NOTE: JWK is already verified to be active at this point - - if (!kms_id) { - throw new Error("KMS ID not found."); - } - - const response = await client.send( - new SignCommand({ - KeyId: kms_id, - Message: new Uint8Array(Buffer.from(encodedHeaderPayload)), - MessageType: "RAW", - SigningAlgorithm: "RSASSA_PKCS1_V1_5_SHA_256", - }), - ); - - if (response?.Signature) { - // See: https://www.rfc-editor.org/rfc/rfc7515#appendix-C - const encodedSignature = base64url - .encode(response.Signature) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=/g, ""); - return `${encodedHeaderPayload}.${encodedSignature}`; - } - } catch (error) { - logger.error("Error signing JWT:", { error }); - } -}; - export const scheduleKeyDeletion = async (client: KMSClient, keyId: string) => { try { await client.send( diff --git a/web/api/helpers/oidc/graphql/fetch-app-secret-query.generated.ts b/web/api/helpers/oidc/graphql/fetch-app-secret-query.generated.ts deleted file mode 100644 index fc9a5e3ab..000000000 --- a/web/api/helpers/oidc/graphql/fetch-app-secret-query.generated.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type FetchAppSecretQueryVariables = Types.Exact<{ - app_id: Types.Scalars["String"]["input"]; -}>; - -export type FetchAppSecretQuery = { - __typename?: "query_root"; - app: Array<{ - __typename?: "app"; - id: string; - actions: Array<{ __typename?: "action"; client_secret: string }>; - }>; -}; - -export const FetchAppSecretDocument = gql` - query FetchAppSecret($app_id: String!) { - app( - where: { - id: { _eq: $app_id } - status: { _eq: "active" } - is_archived: { _eq: false } - engine: { _eq: "cloud" } - } - ) { - id - actions(limit: 1, where: { action: { _eq: "" } }) { - client_secret - } - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - FetchAppSecret( - variables: FetchAppSecretQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request( - FetchAppSecretDocument, - variables, - { ...requestHeaders, ...wrappedRequestHeaders }, - ), - "FetchAppSecret", - "query", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/helpers/oidc/graphql/fetch-app-secret-query.gql b/web/api/helpers/oidc/graphql/fetch-app-secret-query.gql deleted file mode 100644 index abda88e8e..000000000 --- a/web/api/helpers/oidc/graphql/fetch-app-secret-query.gql +++ /dev/null @@ -1,15 +0,0 @@ -query FetchAppSecret($app_id: String!) { - app( - where: { - id: { _eq: $app_id } - status: { _eq: "active" } - is_archived: { _eq: false } - engine: { _eq: "cloud" } - } - ) { - id - actions(limit: 1, where: { action: { _eq: "" } }) { - client_secret - } - } -} diff --git a/web/api/helpers/oidc/graphql/fetch-oidc-app.generated.ts b/web/api/helpers/oidc/graphql/fetch-oidc-app.generated.ts deleted file mode 100644 index 272cd073b..000000000 --- a/web/api/helpers/oidc/graphql/fetch-oidc-app.generated.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type FetchOidcAppQueryVariables = Types.Exact<{ - app_id: Types.Scalars["String"]["input"]; - redirect_uri: Types.Scalars["String"]["input"]; -}>; - -export type FetchOidcAppQuery = { - __typename?: "query_root"; - app: Array<{ - __typename?: "app"; - id: string; - is_staging: boolean; - actions: Array<{ - __typename?: "action"; - id: string; - external_nullifier: string; - status: string; - redirects: Array<{ __typename?: "redirect"; redirect_uri: string }>; - }>; - }>; -}; - -export const FetchOidcAppDocument = gql` - query FetchOIDCApp($app_id: String!, $redirect_uri: String!) { - app( - where: { - id: { _eq: $app_id } - status: { _eq: "active" } - is_archived: { _eq: false } - engine: { _eq: "cloud" } - } - ) { - id - is_staging - actions(where: { action: { _eq: "" } }) { - id - external_nullifier - status - redirects(where: { redirect_uri: { _eq: $redirect_uri } }) { - redirect_uri - } - } - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - FetchOIDCApp( - variables: FetchOidcAppQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request(FetchOidcAppDocument, variables, { - ...requestHeaders, - ...wrappedRequestHeaders, - }), - "FetchOIDCApp", - "query", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/helpers/oidc/graphql/fetch-oidc-app.gql b/web/api/helpers/oidc/graphql/fetch-oidc-app.gql deleted file mode 100644 index b82ab3ec6..000000000 --- a/web/api/helpers/oidc/graphql/fetch-oidc-app.gql +++ /dev/null @@ -1,21 +0,0 @@ -query FetchOIDCApp($app_id: String!, $redirect_uri: String!) { - app( - where: { - id: { _eq: $app_id } - status: { _eq: "active" } - is_archived: { _eq: false } - engine: { _eq: "cloud" } - } - ) { - id - is_staging - actions(where: { action: { _eq: "" } }) { - id - external_nullifier - status - redirects(where: { redirect_uri: { _eq: $redirect_uri } }) { - redirect_uri - } - } - } -} diff --git a/web/api/helpers/oidc/graphql/insert-auth-code.generated.ts b/web/api/helpers/oidc/graphql/insert-auth-code.generated.ts deleted file mode 100644 index 15f82d9a7..000000000 --- a/web/api/helpers/oidc/graphql/insert-auth-code.generated.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type InsertAuthCodeMutationVariables = Types.Exact<{ - auth_code: Types.Scalars["String"]["input"]; - code_challenge?: Types.InputMaybe; - code_challenge_method?: Types.InputMaybe; - expires_at: Types.Scalars["timestamptz"]["input"]; - nullifier_hash: Types.Scalars["String"]["input"]; - app_id: Types.Scalars["String"]["input"]; - verification_level: Types.Scalars["String"]["input"]; - scope: Types.Scalars["jsonb"]["input"]; - nonce?: Types.InputMaybe; - redirect_uri?: Types.InputMaybe; -}>; - -export type InsertAuthCodeMutation = { - __typename?: "mutation_root"; - insert_auth_code_one?: { - __typename?: "auth_code"; - auth_code: string; - nonce?: string | null; - } | null; -}; - -export const InsertAuthCodeDocument = gql` - mutation InsertAuthCode( - $auth_code: String! - $code_challenge: String - $code_challenge_method: String - $expires_at: timestamptz! - $nullifier_hash: String! - $app_id: String! - $verification_level: String! - $scope: jsonb! - $nonce: String - $redirect_uri: String - ) { - insert_auth_code_one( - object: { - auth_code: $auth_code - code_challenge: $code_challenge - code_challenge_method: $code_challenge_method - expires_at: $expires_at - nullifier_hash: $nullifier_hash - app_id: $app_id - verification_level: $verification_level - scope: $scope - nonce: $nonce - redirect_uri: $redirect_uri - } - ) { - auth_code - nonce - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - InsertAuthCode( - variables: InsertAuthCodeMutationVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request( - InsertAuthCodeDocument, - variables, - { ...requestHeaders, ...wrappedRequestHeaders }, - ), - "InsertAuthCode", - "mutation", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/helpers/oidc/graphql/insert-auth-code.gql b/web/api/helpers/oidc/graphql/insert-auth-code.gql deleted file mode 100644 index 2d1d5c890..000000000 --- a/web/api/helpers/oidc/graphql/insert-auth-code.gql +++ /dev/null @@ -1,30 +0,0 @@ -mutation InsertAuthCode( - $auth_code: String! - $code_challenge: String - $code_challenge_method: String - $expires_at: timestamptz! - $nullifier_hash: String! - $app_id: String! - $verification_level: String! - $scope: jsonb! - $nonce: String - $redirect_uri: String -) { - insert_auth_code_one( - object: { - auth_code: $auth_code - code_challenge: $code_challenge - code_challenge_method: $code_challenge_method - expires_at: $expires_at - nullifier_hash: $nullifier_hash - app_id: $app_id - verification_level: $verification_level - scope: $scope - nonce: $nonce - redirect_uri: $redirect_uri - } - ) { - auth_code - nonce - } -} diff --git a/web/api/helpers/oidc/index.ts b/web/api/helpers/oidc/index.ts deleted file mode 100644 index 6cc67a7e5..000000000 --- a/web/api/helpers/oidc/index.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { verifyHashedSecret } from "@/api/helpers/utils"; -import { logger } from "@/lib/logger"; -import { IInternalError, OIDCFlowType, OIDCResponseType } from "@/lib/types"; -import { VerificationLevel } from "@worldcoin/idkit-core"; -import crypto from "crypto"; -import "server-only"; - -import { - FetchAppSecretQuery, - getSdk as FetchAppSecretQuerySdk, -} from "./graphql/fetch-app-secret-query.generated"; - -import { - getSdk as FetchOIDCAppSdk, - FetchOidcAppQuery, -} from "./graphql/fetch-oidc-app.generated"; - -import { getAPIServiceGraphqlClient } from "../graphql"; -import { - InsertAuthCodeMutation, - getSdk as InsertAuthCodeSdk, -} from "./graphql/insert-auth-code.generated"; - -export const OIDCResponseTypeMapping = { - code: OIDCResponseType.Code, - id_token: OIDCResponseType.JWT, - token: OIDCResponseType.JWT, -}; - -export enum OIDCScopes { - OpenID = "openid", - Email = "email", - Profile = "profile", -} - -export enum OIDCErrorCodes { - InvalidRequest = "invalid_request", // RFC6749 OAuth 2.0 (4.1.2.1) - UnsupportedResponseType = "unsupported_response_type", // RFC6749 OAuth 2.0 (4.1.2.1) - InvalidScope = "invalid_scope", // RFC6749 OAuth 2.0 (4.1.2.1) - InvalidRedirectURI = "invalid_redirect_uri", // Custom -} - -interface OIDCApp { - id: FetchOidcAppQuery["app"][number]["id"]; - is_staging: FetchOidcAppQuery["app"][number]["is_staging"]; - external_nullifier: FetchOidcAppQuery["app"][number]["actions"][number]["external_nullifier"]; - action_id: FetchOidcAppQuery["app"][number]["actions"][number]["id"]; - registered_redirect_uri?: FetchOidcAppQuery["app"][number]["actions"][number]["redirects"][number]["redirect_uri"]; -} - -export const fetchOIDCApp = async ( - app_id: string, - redirect_uri: string, -): Promise<{ app?: OIDCApp; error?: IInternalError }> => { - const client = await getAPIServiceGraphqlClient(); - - let data: FetchOidcAppQuery | null = null; - - try { - data = await FetchOIDCAppSdk(client).FetchOIDCApp({ - app_id, - redirect_uri, - }); - } catch (error) { - logger.error("fetchOIDCApp - Failed to fetch OIDC app.", { error }); - - return { - error: { - code: "internal_server_error", - message: "Failed to fetch OIDC app.", - statusCode: 500, - }, - }; - } - - if (data.app.length === 0) { - return { - error: { - code: "app_not_found", - message: "App not found or not active.", - statusCode: 404, - }, - }; - } - - const app = data.app[0]; - - if (!app.actions?.length || app.actions[0].status === "inactive") { - return { - error: { - code: "sign_in_not_enabled", - message: "App does not have Sign in with World ID enabled.", - statusCode: 400, - }, - }; - } - - const external_nullifier = app.actions[0].external_nullifier; - const action_id = app.actions[0].id; - const registered_redirect_uri = app.actions[0].redirects[0]?.redirect_uri; - - const sanitizedApp = { ...app }; - const { actions, ...rest } = sanitizedApp; - - return { - app: { - ...rest, - action_id, - external_nullifier, - registered_redirect_uri, - }, - }; -}; - -export const generateOIDCCode = async ( - app_id: string, - nullifier_hash: string, - verification_level: VerificationLevel, - scope: OIDCScopes[], - redirect_uri: string, - code_challenge?: string, - code_challenge_method?: string, - nonce?: string | null, -): Promise => { - // Generate a random code - const auth_code = crypto.randomBytes(12).toString("hex"); - const client = await getAPIServiceGraphqlClient(); - let data: InsertAuthCodeMutation | null = null; - - try { - data = await InsertAuthCodeSdk(client).InsertAuthCode({ - app_id, - auth_code, - code_challenge, - code_challenge_method, - expires_at: new Date(Date.now() + 1000 * 60 * 10).toISOString(), // 10 minutes - nullifier_hash, - verification_level, - scope, - nonce, - redirect_uri, - }); - } catch (error) { - logger.error("generateOIDCCode - Failed to generate auth code.", { error }); - throw error; - } - - if (data?.insert_auth_code_one?.auth_code !== auth_code) { - throw new Error("Failed to generate auth code."); - } - - return auth_code; -}; - -// TODO: Hash secrets as passwords (e.g. `PBKDF2`) instead of HMAC -export const authenticateOIDCEndpoint = async ( - auth_header: string, -): Promise => { - const authToken = auth_header.replace("Basic ", ""); - const [app_id, client_secret] = Buffer.from(authToken, "base64") - .toString() - .split(":"); - - // Fetch app - const client = await getAPIServiceGraphqlClient(); - - let data: FetchAppSecretQuery | null = null; - - try { - data = await FetchAppSecretQuerySdk(client).FetchAppSecret({ - app_id, - }); - } catch (error) { - logger.error("authenticateOIDCEndpoint - Failed to fetch app.", { error }); - return null; - } - - if (data.app.length === 0) { - logger.info("authenticateOIDCEndpoint - App not found or not active."); - return null; - } - - const hmac_secret = data.app[0]?.actions?.[0]?.client_secret; - - if (!hmac_secret) { - logger.info( - "authenticateOIDCEndpoint - App does not have Sign in with World ID enabled.", - ); - return null; - } - - // ANCHOR: Verify client secret - if (!verifyHashedSecret(app_id, client_secret, hmac_secret)) { - logger.warn("authenticateOIDCEndpoint - Invalid client secret."); - return null; - } - - return app_id; -}; - -export function checkFlowType(responseTypes: string[]) { - const includesAll = (requiredParams: string[]): boolean => { - return requiredParams.every((param) => responseTypes.includes(param)); - }; - - // NOTE: List of valid response types for the hybrid flow - // Source: https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth:~:text=this%20value%20is%20code%C2%A0id_token%2C%20code%C2%A0token%2C%20or%20code%C2%A0id_token%C2%A0token. - if ( - includesAll([OIDCResponseType.Code, OIDCResponseType.IdToken]) || - includesAll([OIDCResponseType.Code, OIDCResponseType.Token]) || - includesAll([ - OIDCResponseType.Code, - OIDCResponseType.IdToken, - OIDCResponseType.Token, - ]) - ) { - return OIDCFlowType.Hybrid; - } - - // NOTE: List of valid response types for the code flow - // Source: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth:~:text=Authorization%20Code%20Flow%2C-,this%20value%20is%20code.,-client_id - if (includesAll([OIDCResponseType.Code])) { - return OIDCFlowType.AuthorizationCode; - } - - // NOTE: List of valid response types for the implicit flow - // Source: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth:~:text=this%20value%20is%20id_token%C2%A0token%20or%20id_token - if ( - includesAll([OIDCResponseType.IdToken]) || - includesAll([OIDCResponseType.IdToken, OIDCResponseType.Token]) - ) { - return OIDCFlowType.Implicit; - } - - if (includesAll([OIDCResponseType.Token])) { - return OIDCFlowType.Token; - } - - return null; -} diff --git a/web/api/v1/jwks/graphql/get-jwks.generated.ts b/web/api/v1/jwks/graphql/get-jwks.generated.ts deleted file mode 100644 index 25301f89a..000000000 --- a/web/api/v1/jwks/graphql/get-jwks.generated.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type JwkQueryQueryVariables = Types.Exact<{ [key: string]: never }>; - -export type JwkQueryQuery = { - __typename?: "query_root"; - jwks: Array<{ - __typename?: "jwks"; - id: string; - expires_at: string; - key: any; - }>; -}; - -export const JwkQueryDocument = gql` - query JWKQuery { - jwks { - id - expires_at - key: public_jwk - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - JWKQuery( - variables?: JwkQueryQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request(JwkQueryDocument, variables, { - ...requestHeaders, - ...wrappedRequestHeaders, - }), - "JWKQuery", - "query", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/v1/jwks/graphql/get-jwks.graphql b/web/api/v1/jwks/graphql/get-jwks.graphql deleted file mode 100644 index eb3d16db2..000000000 --- a/web/api/v1/jwks/graphql/get-jwks.graphql +++ /dev/null @@ -1,7 +0,0 @@ -query JWKQuery { - jwks { - id - expires_at - key: public_jwk - } -} diff --git a/web/api/v1/jwks/index.ts b/web/api/v1/jwks/index.ts deleted file mode 100644 index 96fc87413..000000000 --- a/web/api/v1/jwks/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getAPIServiceGraphqlClient } from "@/api/helpers/graphql"; -import { corsHandler } from "@/api/helpers/utils"; -import { NextRequest, NextResponse } from "next/server"; -import { getSdk as getJWKsSdk } from "./graphql/get-jwks.generated"; - -const corsMethods = ["GET", "OPTIONS"]; - -/** - * Retrieves JWKs to verify proofs - * @param req - * @param res - */ -export async function GET(req: NextRequest) { - const client = await getAPIServiceGraphqlClient(); - const getJWKs = getJWKsSdk(client); - const response = await getJWKs.JWKQuery(); - - const keys = []; - for (const { id, key } of response.jwks) { - keys.push({ ...key, kid: id }); - } - - return corsHandler(NextResponse.json({ keys }), corsMethods); -} - -export async function OPTIONS(req: NextRequest) { - return corsHandler(new NextResponse(null, { status: 204 }), corsMethods); -} diff --git a/web/api/v1/oidc/authorize/graphql/fetch-nullifier.generated.ts b/web/api/v1/oidc/authorize/graphql/fetch-nullifier.generated.ts deleted file mode 100644 index 336bd1e05..000000000 --- a/web/api/v1/oidc/authorize/graphql/fetch-nullifier.generated.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type NullifierQueryVariables = Types.Exact<{ - nullifier_hash: Types.Scalars["String"]["input"]; -}>; - -export type NullifierQuery = { - __typename?: "query_root"; - nullifier: Array<{ __typename?: "nullifier"; id: string }>; -}; - -export const NullifierDocument = gql` - query Nullifier($nullifier_hash: String!) { - nullifier(where: { nullifier_hash: { _eq: $nullifier_hash } }) { - id - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - Nullifier( - variables: NullifierQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request(NullifierDocument, variables, { - ...requestHeaders, - ...wrappedRequestHeaders, - }), - "Nullifier", - "query", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/v1/oidc/authorize/graphql/fetch-nullifier.graphql b/web/api/v1/oidc/authorize/graphql/fetch-nullifier.graphql deleted file mode 100644 index 3ec6c2039..000000000 --- a/web/api/v1/oidc/authorize/graphql/fetch-nullifier.graphql +++ /dev/null @@ -1,5 +0,0 @@ -query Nullifier($nullifier_hash: String!) { - nullifier(where: { nullifier_hash: { _eq: $nullifier_hash } }) { - id - } -} diff --git a/web/api/v1/oidc/authorize/graphql/upsert-nullifier.generated.ts b/web/api/v1/oidc/authorize/graphql/upsert-nullifier.generated.ts deleted file mode 100644 index 0becf53f3..000000000 --- a/web/api/v1/oidc/authorize/graphql/upsert-nullifier.generated.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type UpsertNullifierMutationVariables = Types.Exact<{ - object: Types.Nullifier_Insert_Input; - on_conflict: Types.Nullifier_On_Conflict; -}>; - -export type UpsertNullifierMutation = { - __typename?: "mutation_root"; - insert_nullifier_one?: { - __typename?: "nullifier"; - id: string; - nullifier_hash: string; - } | null; -}; - -export const UpsertNullifierDocument = gql` - mutation UpsertNullifier( - $object: nullifier_insert_input! - $on_conflict: nullifier_on_conflict! - ) { - insert_nullifier_one(object: $object, on_conflict: $on_conflict) { - id - nullifier_hash - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - UpsertNullifier( - variables: UpsertNullifierMutationVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request( - UpsertNullifierDocument, - variables, - { ...requestHeaders, ...wrappedRequestHeaders }, - ), - "UpsertNullifier", - "mutation", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/v1/oidc/authorize/graphql/upsert-nullifier.graphql b/web/api/v1/oidc/authorize/graphql/upsert-nullifier.graphql deleted file mode 100644 index 92472526a..000000000 --- a/web/api/v1/oidc/authorize/graphql/upsert-nullifier.graphql +++ /dev/null @@ -1,9 +0,0 @@ -mutation UpsertNullifier( - $object: nullifier_insert_input! - $on_conflict: nullifier_on_conflict! -) { - insert_nullifier_one(object: $object, on_conflict: $on_conflict) { - id - nullifier_hash - } -} diff --git a/web/api/v1/oidc/authorize/index.ts b/web/api/v1/oidc/authorize/index.ts deleted file mode 100644 index 934db0c32..000000000 --- a/web/api/v1/oidc/authorize/index.ts +++ /dev/null @@ -1,439 +0,0 @@ -import { errorResponse } from "@/api/helpers/errors"; -import { getAPIServiceGraphqlClient } from "@/api/helpers/graphql"; -import { fetchActiveJWK } from "@/api/helpers/jwks"; -import { generateOIDCJWT } from "@/api/helpers/jwts"; -import { - OIDCErrorCodes, - OIDCResponseTypeMapping, - OIDCScopes, - checkFlowType, - fetchOIDCApp, - generateOIDCCode, -} from "@/api/helpers/oidc"; -import { corsHandler } from "@/api/helpers/utils"; -import { validateRequestSchema } from "@/api/helpers/validate-request-schema"; -import { encodeNullifierForStorage, verifyProof } from "@/api/helpers/verify"; -import { Nullifier_Constraint } from "@/graphql/graphql"; -import { logger } from "@/lib/logger"; -import { OIDCFlowType, OIDCResponseType } from "@/lib/types"; -import { captureEvent } from "@/services/posthogClient"; -import { VerificationLevel } from "@worldcoin/idkit-core"; -import { hashToField } from "@worldcoin/idkit-core/hashing"; -import { createHash } from "crypto"; -import { toBeHex } from "ethers"; -import { NextRequest, NextResponse } from "next/server"; -import * as yup from "yup"; -import { getSdk as getNullifierSdk } from "./graphql/fetch-nullifier.generated"; -import { getSdk as getUpsertNullifierSdk } from "./graphql/upsert-nullifier.generated"; - -// NOTE: This endpoint should only be called from Sign in with World, params follow World ID conventions. Sign in with World handles OIDC requests. -const schema = yup - .object({ - proof: yup.string().strict().required("This attribute is required."), - nullifier_hash: yup - .string() - .strict() - .required("This attribute is required."), - merkle_root: yup.string().strict().required("This attribute is required."), - verification_level: yup - .string() - .oneOf(Object.values(VerificationLevel)) - .required("This attribute is required."), - app_id: yup.string().strict().required("This attribute is required."), - signal: yup // `signal` in the context of World ID; `nonce` in the context of OIDC - .string() - .ensure() - .when("response_type", { - is: (response_type: string) => - !["code", "code token"].includes(response_type), - then: (nonce) => - nonce.required( - "`nonce` required for all response types except `code` and `code token`.", - ), - }), // NOTE: nonce is required for all response types except `code` and `code token` - code_challenge: yup.string(), - code_challenge_method: yup.string(), - scope: yup - .string() - .strict() - .required("The openid scope is always required."), - response_type: yup - .string() - .strict() - .required("This attribute is required."), - redirect_uri: yup.string().strict().required("This attribute is required."), - }) - .noUnknown(); - -const corsMethods = ["POST", "OPTIONS"]; -/** - * Authenticates a "Sign in with World ID" user with a ZKP and issues a JWT or a code (authorization code flow) - * This endpoint is called by the Sign in with World ID page (or the app's own page if using IDKit [advanced]) - */ -export async function POST(req: NextRequest) { - const redis = global.RedisClient; - - if (!redis) { - return corsHandler( - errorResponse({ - statusCode: 500, - code: "internal_server_error", - detail: "Redis client not found", - attribute: "server", - req, - }), - corsMethods, - ); - } - - let app_id: string | undefined; - - try { - const body = await req.json(); - const { isValid, parsedParams, handleError } = await validateRequestSchema({ - schema, - value: body, - }); - - if (!isValid) { - return handleError(req); - } - - app_id = parsedParams.app_id; - - const { - proof, - nullifier_hash, - merkle_root, - signal, - verification_level, - response_type, - scope, - redirect_uri, - code_challenge, - code_challenge_method, - } = parsedParams; - - const response_types = decodeURIComponent( - (response_type as string | string[]).toString(), - ).split(" "); - - for (const response_type of response_types) { - if (!Object.keys(OIDCResponseTypeMapping).includes(response_type)) { - return corsHandler( - errorResponse({ - statusCode: 400, - code: OIDCErrorCodes.UnsupportedResponseType, - detail: `Invalid response type: ${response_type}.`, - attribute: "response_type", - req, - app_id, - }), - corsMethods, - ); - } - } - - if (code_challenge && code_challenge_method !== "S256") { - return corsHandler( - errorResponse({ - statusCode: 400, - code: OIDCErrorCodes.InvalidRequest, - detail: `Invalid code_challenge_method: ${code_challenge_method}.`, - attribute: "code_challenge_method", - req, - app_id, - }), - corsMethods, - ); - } - - const scopes = decodeURIComponent( - (scope as string | string[])?.toString(), - ).split(" ") as OIDCScopes[]; - const sanitizedScopes: OIDCScopes[] = scopes.length - ? [ - ...new Set( - // NOTE: Invalid scopes are ignored per spec (3.1.2.1) - scopes.filter((scope) => Object.values(OIDCScopes).includes(scope)), - ), - ] - : []; - - if ( - !sanitizedScopes.length || - !sanitizedScopes.includes(OIDCScopes.OpenID) - ) { - return corsHandler( - errorResponse({ - statusCode: 400, - code: OIDCErrorCodes.InvalidScope, - detail: `The ${OIDCScopes.OpenID} scope is always required.`, - attribute: "scope", - req, - app_id, - }), - corsMethods, - ); - } - - // ANCHOR: Check the app is valid and fetch information - const { app, error: fetchAppError } = await fetchOIDCApp( - app_id, - redirect_uri, - ); - if (!app || fetchAppError) { - return corsHandler( - errorResponse({ - statusCode: fetchAppError?.statusCode ?? 400, - code: fetchAppError?.code ?? "error", - detail: fetchAppError?.message ?? "Error fetching app.", - attribute: fetchAppError?.attribute ?? "app_id", - req, - app_id, - }), - corsMethods, - ); - } - - // ANCHOR: Verify redirect URI is valid - if (app.registered_redirect_uri !== redirect_uri) { - return corsHandler( - errorResponse({ - statusCode: 400, - code: OIDCErrorCodes.InvalidRedirectURI, - detail: "Invalid redirect URI.", - attribute: "redirect_uri", - req, - app_id, - }), - corsMethods, - ); - } - - // Anchor: Check the proof hasn't been replayed - let hashedProof: string; - try { - hashedProof = createHash("sha256").update(proof).digest("hex"); - } catch (error) { - return corsHandler( - errorResponse({ - statusCode: 400, - code: "invalid_proof", - detail: "Provided proof is invalid.", - attribute: "proof", - req, - app_id, - }), - corsMethods, - ); - } - const proofKey = `oidc:proof:${hashedProof}`; - const isProofReplayed = await redis.get(proofKey); - - if (isProofReplayed) { - return corsHandler( - errorResponse({ - statusCode: 400, - code: "invalid_proof", - detail: "This proof has already been used. Please try again", - attribute: "proof", - req, - app_id, - }), - corsMethods, - ); - } - - // Set the proof before continuing with other operations - await redis.set(proofKey, "1", "EX", 5400); - - // For OIDC we should always hash the signal now. - let signalHash: string; - try { - signalHash = toBeHex(hashToField(signal).hash as bigint); - } catch (error) { - return corsHandler( - errorResponse({ - statusCode: 400, - code: "invalid_signal", - detail: "Provided signal is invalid.", - attribute: "signal", - req, - app_id, - }), - corsMethods, - ); - } - - // ANCHOR: Verify the zero-knowledge proof - const { error: verifyError } = await verifyProof( - { - proof, - nullifier_hash, - merkle_root, - signal_hash: signalHash, - external_nullifier: app.external_nullifier, - }, - { - is_staging: app.is_staging, - verification_level, - max_age: 3600, // require that root be less than 1 hour old - }, - ); - - if (verifyError) { - return corsHandler( - errorResponse({ - statusCode: verifyError.statusCode ?? 400, - code: verifyError.code ?? "invalid_proof", - detail: - verifyError.message ?? - "Verification request error. Please try again.", - attribute: verifyError.attribute, - req, - app_id, - }), - corsMethods, - ); - } - - // ANCHOR: Proof is valid, issue relevant codes - const response = {} as { code?: string; id_token?: string; token?: string }; - - if (response_types.includes(OIDCResponseType.Code)) { - const shouldStoreSignal = - checkFlowType(response_types) === OIDCFlowType.AuthorizationCode && - signal; - - response.code = await generateOIDCCode( - app.id, - nullifier_hash, - verification_level, - sanitizedScopes, - redirect_uri, - code_challenge, - code_challenge_method, - shouldStoreSignal ? signal : null, - ); - } - - let jwt: string | undefined; - for (const response_type of response_types) { - if ( - OIDCResponseTypeMapping[ - response_type as keyof typeof OIDCResponseTypeMapping - ] === OIDCResponseType.JWT - ) { - if (!jwt) { - const jwk = await fetchActiveJWK(); - - jwt = await generateOIDCJWT({ - app_id: app.id, - nullifier_hash, - verification_level, - nonce: signal, - scope: sanitizedScopes, - kid: jwk.kid, - kms_id: jwk.kms_id ?? "", - }); - } - - response[response_type as keyof typeof OIDCResponseTypeMapping] = jwt; - } - } - - const client = await getAPIServiceGraphqlClient(); - const nullifierSdk = getNullifierSdk(client); - const upsertNullifierSdk = getUpsertNullifierSdk(client); - - let hasNullifier: boolean = false; - - try { - const fetchNullifierResult = await nullifierSdk.Nullifier({ - nullifier_hash, - }); - - if (!fetchNullifierResult?.nullifier) { - logger.warn("Error fetching nullifier.", { - fetchNullifierResult: fetchNullifierResult ?? {}, - app_id, - }); - hasNullifier = false; - } - hasNullifier = Boolean(fetchNullifierResult.nullifier?.[0]?.id); - } catch (error) { - // Temp Fix to reduce on call alerts - logger.warn("Query error nullifier.", { - nullifier_hash, - errorMessage: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - app_id, - }); - } - - if (!hasNullifier) { - try { - const { insert_nullifier_one } = - await upsertNullifierSdk.UpsertNullifier({ - object: { - nullifier_hash, - action_id: app.action_id, - nullifier_hash_int: encodeNullifierForStorage(nullifier_hash), - }, - on_conflict: { - constraint: Nullifier_Constraint.NullifierPkey, - }, - }); - - if (!insert_nullifier_one) { - logger.error("Error inserting nullifier.", { - insert_nullifier_one: insert_nullifier_one ?? {}, - app_id, - }); - } - } catch (error) { - logger.error("Generic Error inserting nullifier", { - req, - error, - app_id, - }); - } - } - - await captureEvent({ - event: "world_id_sign_in_success", - distinctId: app.id, - properties: { - verification_level: verification_level, - }, - }); - - return corsHandler(NextResponse.json(response, { status: 200 }), [ - "POST", - "OPTIONS", - ]); - } catch (error) { - // Handle any unexpected errors - logger.error("Unexpected error in OIDC authorize", { - error, - app_id, - }); - - return corsHandler( - errorResponse({ - statusCode: 500, - code: "internal_server_error", - detail: "An unexpected error occurred", - attribute: "server", - req, - app_id, - }), - corsMethods, - ); - } -} - -export async function OPTIONS(req: NextRequest) { - return corsHandler(new NextResponse(null, { status: 204 }), corsMethods); -} diff --git a/web/api/v1/oidc/introspect/index.ts b/web/api/v1/oidc/introspect/index.ts deleted file mode 100644 index 2f9393ab2..000000000 --- a/web/api/v1/oidc/introspect/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - errorResponse, - errorUnauthenticated, - errorValidation, -} from "@/api/helpers/errors"; -import { verifyOIDCJWT } from "@/api/helpers/jwts"; -import { authenticateOIDCEndpoint } from "@/api/helpers/oidc"; -import { validateRequestSchema } from "@/api/helpers/validate-request-schema"; -import { NextRequest, NextResponse } from "next/server"; -import * as yup from "yup"; - -const schema = yup - .object({ - token: yup.string().strict().required("This attribute is required."), - }) - .noUnknown(); - -export async function POST(req: NextRequest) { - if (req.headers.get("content-type") !== "application/x-www-form-urlencoded") { - return errorValidation( - "invalid_content_type", - "Invalid content type. Only application/x-www-form-urlencoded is supported.", - null, - req, - ); - } - - const { isValid, parsedParams, handleError } = await validateRequestSchema({ - schema, - value: req.body, - }); - - if (!isValid) { - return handleError(req); - } - - const userToken = parsedParams.token; - - // ANCHOR: Authenticate the request comes from the app - const authHeader = req.headers.get("authorization"); - - if (!authHeader) { - return errorUnauthenticated( - "Please provide your app authentication credentials.", - req, - ); - } - - let app_id: string | null; - app_id = await authenticateOIDCEndpoint(authHeader); - - if (!app_id) { - return errorUnauthenticated("Invalid authentication credentials.", req); - } - - try { - const payload = await verifyOIDCJWT(userToken); - - return NextResponse.json({ - active: true, - client_id: app_id, - exp: payload.exp, - sub: payload.sub, - }); - } catch { - return errorResponse({ - statusCode: 401, - code: "invalid_token", - detail: "Token is invalid or expired.", - attribute: "token", - req, - app_id, - }); - } -} diff --git a/web/api/v1/oidc/openid-configuration/index.ts b/web/api/v1/oidc/openid-configuration/index.ts deleted file mode 100644 index a0c74103d..000000000 --- a/web/api/v1/oidc/openid-configuration/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { JWT_ISSUER } from "@/api/helpers/jwts"; -import { OIDCScopes } from "@/api/helpers/oidc"; -import { OIDC_BASE_URL } from "@/lib/constants"; -import { NextRequest, NextResponse } from "next/server"; - -/** - * Returns an OpenID Connect discovery document, according to spec - * https://openid.net/specs/openid-connect-discovery-1_0.html - * @param req - */ -export async function GET(req: NextRequest) { - return NextResponse.json({ - issuer: JWT_ISSUER, - jwks_uri: `${OIDC_BASE_URL}/jwks.json`, - token_endpoint: `${OIDC_BASE_URL}/token`, - code_challenge_methods_supported: ["S256"], - scopes_supported: Object.values(OIDCScopes), - id_token_signing_alg_values_supported: ["RSA"], - userinfo_endpoint: `${OIDC_BASE_URL}/userinfo`, - authorization_endpoint: `${OIDC_BASE_URL}/authorize`, - grant_types_supported: ["authorization_code", "implicit"], - service_documentation: "https://docs.world.org/world-id", - op_policy_uri: "https://developer.world.org/privacy-statement", - op_tos_uri: "https://developer.world.org/tos", - subject_types_supported: ["pairwise"], // subject is unique to each application, cannot be used across - response_modes_supported: ["query", "fragment", "form_post"], - response_types_supported: [ - "code", // Authorization code flow - "id_token", // Implicit flow - "id_token token", // Implicit flow - "code id_token", // Hybrid flow - ], - }); -} - -export async function OPTIONS(req: NextRequest) { - return NextResponse.json(null, { status: 204 }); -} diff --git a/web/api/v1/oidc/token/graphql/delete-auth-code.generated.ts b/web/api/v1/oidc/token/graphql/delete-auth-code.generated.ts deleted file mode 100644 index db825c384..000000000 --- a/web/api/v1/oidc/token/graphql/delete-auth-code.generated.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type DeleteAuthCodeMutationVariables = Types.Exact<{ - auth_code: Types.Scalars["String"]["input"]; - app_id: Types.Scalars["String"]["input"]; - now: Types.Scalars["timestamptz"]["input"]; -}>; - -export type DeleteAuthCodeMutation = { - __typename?: "mutation_root"; - delete_auth_code?: { - __typename?: "auth_code_mutation_response"; - affected_rows: number; - returning: Array<{ - __typename?: "auth_code"; - nullifier_hash: string; - verification_level: string; - scope?: any | null; - code_challenge?: string | null; - code_challenge_method?: string | null; - redirect_uri: string; - nonce?: string | null; - }>; - } | null; -}; - -export const DeleteAuthCodeDocument = gql` - mutation DeleteAuthCode( - $auth_code: String! - $app_id: String! - $now: timestamptz! - ) { - delete_auth_code( - where: { - app_id: { _eq: $app_id } - expires_at: { _gt: $now } - auth_code: { _eq: $auth_code } - } - ) { - returning { - nullifier_hash - verification_level - scope - code_challenge - code_challenge_method - redirect_uri - nonce - } - affected_rows - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - DeleteAuthCode( - variables: DeleteAuthCodeMutationVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request( - DeleteAuthCodeDocument, - variables, - { ...requestHeaders, ...wrappedRequestHeaders }, - ), - "DeleteAuthCode", - "mutation", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/v1/oidc/token/graphql/delete-auth-code.graphql b/web/api/v1/oidc/token/graphql/delete-auth-code.graphql deleted file mode 100644 index 167833fb7..000000000 --- a/web/api/v1/oidc/token/graphql/delete-auth-code.graphql +++ /dev/null @@ -1,24 +0,0 @@ -mutation DeleteAuthCode( - $auth_code: String! - $app_id: String! - $now: timestamptz! -) { - delete_auth_code( - where: { - app_id: { _eq: $app_id } - expires_at: { _gt: $now } - auth_code: { _eq: $auth_code } - } - ) { - returning { - nullifier_hash - verification_level - scope - code_challenge - code_challenge_method - redirect_uri - nonce - } - affected_rows - } -} diff --git a/web/api/v1/oidc/token/graphql/fetch-redirect-count.generated.ts b/web/api/v1/oidc/token/graphql/fetch-redirect-count.generated.ts deleted file mode 100644 index 58394fb0b..000000000 --- a/web/api/v1/oidc/token/graphql/fetch-redirect-count.generated.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable import/no-relative-parent-imports -- auto generated file */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type FetchRedirectCountQueryQueryVariables = Types.Exact<{ - app_id?: Types.InputMaybe; -}>; - -export type FetchRedirectCountQueryQuery = { - __typename?: "query_root"; - action: Array<{ __typename?: "action"; redirect_count?: number | null }>; -}; - -export const FetchRedirectCountQueryDocument = gql` - query FetchRedirectCountQuery($app_id: String) { - action(where: { app_id: { _eq: $app_id }, action: { _eq: "" } }) { - redirect_count - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - FetchRedirectCountQuery( - variables?: FetchRedirectCountQueryQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request( - FetchRedirectCountQueryDocument, - variables, - { ...requestHeaders, ...wrappedRequestHeaders }, - ), - "FetchRedirectCountQuery", - "query", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/api/v1/oidc/token/graphql/fetch-redirect-count.graphql b/web/api/v1/oidc/token/graphql/fetch-redirect-count.graphql deleted file mode 100644 index ff511db8e..000000000 --- a/web/api/v1/oidc/token/graphql/fetch-redirect-count.graphql +++ /dev/null @@ -1,5 +0,0 @@ -query FetchRedirectCountQuery($app_id: String) { - action(where: { app_id: { _eq: $app_id }, action: { _eq: "" } }) { - redirect_count - } -} diff --git a/web/api/v1/oidc/token/index.ts b/web/api/v1/oidc/token/index.ts deleted file mode 100644 index d299b991f..000000000 --- a/web/api/v1/oidc/token/index.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { errorOIDCResponse } from "@/api/helpers/errors"; -import { getAPIServiceGraphqlClient } from "@/api/helpers/graphql"; -import { fetchActiveJWK } from "@/api/helpers/jwks"; -import { generateOIDCJWT } from "@/api/helpers/jwts"; -import { authenticateOIDCEndpoint } from "@/api/helpers/oidc"; -import { corsHandler } from "@/api/helpers/utils"; -import { validateRequestSchema } from "@/api/helpers/validate-request-schema"; -import { VerificationLevel } from "@worldcoin/idkit-core"; -import { createHash, timingSafeEqual } from "crypto"; -import { NextRequest, NextResponse } from "next/server"; -import * as yup from "yup"; -import { getSdk as getDeleteAuthCodeSdk } from "./graphql/delete-auth-code.generated"; -import { getSdk as getFetchRedirectCountSdk } from "./graphql/fetch-redirect-count.generated"; - -const corsMethods = ["POST", "OPTIONS"]; -const schema = yup - .object({ - grant_type: yup.string().default("authorization_code"), - code: yup.string().strict().required("This attribute is required."), - redirect_uri: yup.string().notRequired(), - client_id: yup.string().notRequired(), - client_secret: yup.string().notRequired(), - code_verifier: yup.string().notRequired(), - }) - .noUnknown(); - -export async function POST(req: NextRequest) { - if ( - !req.headers - .get("content-type") - ?.toLowerCase() - .startsWith("application/x-www-form-urlencoded") - ) { - const sanitizedContentType = req.headers - .get("content-type") - ?.replace(/\n|\r/g, ""); - console.warn("Invalid content type", sanitizedContentType); - return errorOIDCResponse( - 400, - "invalid_request", - "Invalid content type. Only application/x-www-form-urlencoded is supported.", - null, - req, - ); - } - - // ANCHOR: Authenticate the request - let authToken = req.headers.get("authorization"); - - const rawBody = await req.text(); - const params = new URLSearchParams(rawBody); - const body = Object.fromEntries(params.entries()); - const { isValid, parsedParams, handleError } = await validateRequestSchema({ - schema, - value: body, - }); - - if (!isValid) { - console.log("Invalid request", parsedParams); - return handleError(req); - } - - const { - grant_type, - code, - redirect_uri, - client_id, - client_secret, - code_verifier, - } = parsedParams; - - if (!authToken) { - // Attempt to get the credentials in the request body - if (client_id && client_secret) { - authToken = `Basic ${Buffer.from( - `${client_id}:${client_secret}`, - ).toString("base64")}`; - } - } - - if (!authToken) { - const response = errorOIDCResponse( - 401, - "unauthorized_client", - "Please provide your app authentication credentials.", - null, - req, - ); - return code_verifier ? corsHandler(response, corsMethods) : response; - } - - let app_id: string | null; - app_id = await authenticateOIDCEndpoint(authToken); - - if (!app_id) { - const response = errorOIDCResponse( - 401, - "unauthorized_client", - "Invalid authentication credentials", - null, - req, - ); - return code_verifier ? corsHandler(response, corsMethods) : response; - } - - const client = await getAPIServiceGraphqlClient(); - const deleteAuthCodeSdk = getDeleteAuthCodeSdk(client); - const fetchRedirectCountSdk = getFetchRedirectCountSdk(client); - const now = new Date().toISOString(); - const deleteAuthCodeResult = await deleteAuthCodeSdk.DeleteAuthCode({ - auth_code: code, - app_id, - now, - }); - - if ( - !deleteAuthCodeResult?.delete_auth_code || - !deleteAuthCodeResult.delete_auth_code.returning.length - ) { - const response = errorOIDCResponse( - 400, - "invalid_grant", - "Invalid authorization code.", - null, - req, - app_id, - ); - return code_verifier ? corsHandler(response, corsMethods) : response; - } - - const authCode = deleteAuthCodeResult.delete_auth_code.returning[0]; - - if (!redirect_uri) { - const redirectCountResult = - await fetchRedirectCountSdk.FetchRedirectCountQuery({ - app_id, - }); - if ( - redirectCountResult?.action?.[0]?.redirect_count && - redirectCountResult?.action[0].redirect_count > 1 - ) { - const response = errorOIDCResponse( - 400, - "invalid_request", - "Missing redirect URI.", - "redirect_uri", - req, - app_id, - ); - return code_verifier ? corsHandler(response, corsMethods) : response; - } - } else if (authCode.redirect_uri !== redirect_uri) { - const response = errorOIDCResponse( - 400, - "invalid_request", - "Invalid redirect URI.", - "redirect_uri", - req, - app_id, - ); - return code_verifier ? corsHandler(response, corsMethods) : response; - } - - if (authCode.code_challenge) { - if (!code_verifier) { - const response = errorOIDCResponse( - 400, - "invalid_request", - "Missing code verifier.", - "code_verifier", - req, - app_id, - ); - return code_verifier ? corsHandler(response, corsMethods) : response; - } - - // We only support S256 method - if (!verifyChallenge(authCode.code_challenge, code_verifier)) { - await deleteAuthCodeSdk.DeleteAuthCode({ - auth_code: code, - app_id, - now, - }); - - const response = errorOIDCResponse( - 400, - "invalid_request", - "Invalid code verifier.", - "code_verifier", - req, - app_id, - ); - return code_verifier ? corsHandler(response, corsMethods) : response; - } - } else { - if (code_verifier) { - const response = errorOIDCResponse( - 400, - "invalid_request", - "Code verifier was not expected.", - "code_verifier", - req, - app_id, - ); - return code_verifier ? corsHandler(response, corsMethods) : response; - } - } - - const jwk = await fetchActiveJWK(); - const token = await generateOIDCJWT({ - app_id, - nullifier_hash: authCode.nullifier_hash, - verification_level: authCode.verification_level as VerificationLevel, - kid: jwk.kid, - kms_id: jwk.kms_id ?? "", - scope: authCode.scope, - nonce: authCode.nonce || undefined, - }); - - const response = NextResponse.json({ - access_token: token, - token_type: "Bearer", - expires_in: 3600, - scope: authCode.scope?.join(" ") || "", - id_token: token, - }); - - return code_verifier ? corsHandler(response, corsMethods) : response; -} - -const verifyChallenge = (challenge: string, verifier: string) => { - const hashedVerifier = createHash("sha256") - .update(verifier) - .digest("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=/g, ""); - - return timingSafeEqual(Buffer.from(challenge), Buffer.from(hashedVerifier)); -}; - -export async function OPTIONS(req: NextRequest) { - return corsHandler(new NextResponse(null, { status: 204 }), corsMethods); -} diff --git a/web/api/v1/oidc/userinfo/index.ts b/web/api/v1/oidc/userinfo/index.ts deleted file mode 100644 index b619d3529..000000000 --- a/web/api/v1/oidc/userinfo/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { errorResponse, errorUnauthenticated } from "@/api/helpers/errors"; -import { verifyOIDCJWT } from "@/api/helpers/jwts"; -import { corsHandler } from "@/api/helpers/utils"; -import { NextRequest, NextResponse } from "next/server"; - -const corsMethods = ["GET", "POST", "OPTIONS"]; - -/** - * Handles GET requests for the userinfo endpoint - */ -export async function GET(req: NextRequest) { - const authorization = req.headers.get("authorization"); - if (!authorization) { - return corsHandler( - errorUnauthenticated("Missing credentials.", req), - corsMethods, - ); - } - - const token = authorization.replace("Bearer ", ""); - - try { - const payload = await verifyOIDCJWT(token); - const response: Record = { - sub: payload.sub, - "https://id.worldcoin.org/beta": payload["https://id.worldcoin.org/beta"], - "https://id.worldcoin.org/v1": payload["https://id.worldcoin.org/v1"], - }; - const scopes = (payload.scope as string)?.toString().split(" "); - - if (scopes?.includes("email")) { - response.email = `${payload.sub}@id.worldcoin.org`; - } - - if (scopes?.includes("profile")) { - response.name = "World ID User"; - response.given_name = "World ID"; - response.family_name = "User"; - } - - return corsHandler(NextResponse.json(response), corsMethods); - } catch { - return corsHandler( - errorResponse({ - statusCode: 401, - code: "invalid_token", - detail: "Token is invalid or expired.", - attribute: "token", - req, - }), - corsMethods, - ); - } -} - -/** - * Handles POST requests for the userinfo endpoint - */ -export async function POST(req: NextRequest) { - const authorization = req.headers.get("authorization"); - if (!authorization) { - return corsHandler( - errorUnauthenticated("Missing credentials.", req), - corsMethods, - ); - } - - const token = authorization.replace("Bearer ", ""); - - try { - const payload = await verifyOIDCJWT(token); - const response: Record = { - sub: payload.sub, - "https://id.worldcoin.org/beta": payload["https://id.worldcoin.org/beta"], - "https://id.worldcoin.org/v1": payload["https://id.worldcoin.org/v1"], - }; - const scopes = (payload.scope as string)?.toString().split(" "); - - if (scopes?.includes("email")) { - response.email = `${payload.sub}@id.worldcoin.org`; - } - - if (scopes?.includes("profile")) { - response.name = "World ID User"; - response.given_name = "World ID"; - response.family_name = "User"; - } - - return corsHandler(NextResponse.json(response), corsMethods); - } catch { - return corsHandler( - errorResponse({ - statusCode: 401, - code: "invalid_token", - detail: "Token is invalid or expired.", - attribute: "token", - req, - }), - corsMethods, - ); - } -} - -/** - * Handles OPTIONS requests - */ -export async function OPTIONS(req: NextRequest) { - return corsHandler(new NextResponse(null, { status: 204 }), corsMethods); -} diff --git a/web/api/v1/oidc/validate/index.ts b/web/api/v1/oidc/validate/index.ts deleted file mode 100644 index 033caeb23..000000000 --- a/web/api/v1/oidc/validate/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { errorResponse } from "@/api/helpers/errors"; -import { fetchOIDCApp } from "@/api/helpers/oidc"; -import { validateRequestSchema } from "@/api/helpers/validate-request-schema"; -import { validateUrl } from "@/lib/utils"; -import { NextRequest, NextResponse } from "next/server"; -import * as yup from "yup"; - -const schema = yup - .object({ - app_id: yup.string().strict().required("This attribute is required."), - redirect_uri: yup.string().strict().required("This attribute is required."), - }) - .noUnknown(); - -/** - * Prevalidates app_id & redirect_uri is valid for Sign in with World ID for early user feedback - */ -export async function POST(req: NextRequest) { - const body = await req.json(); - const { isValid, parsedParams, handleError } = await validateRequestSchema({ - schema, - value: body, - }); - - if (!isValid) { - return handleError(req); - } - - const { app_id, redirect_uri } = parsedParams; - - const { app, error: fetchAppError } = await fetchOIDCApp( - app_id, - redirect_uri, - ); - if (!app || fetchAppError) { - return errorResponse({ - statusCode: fetchAppError?.statusCode ?? 400, - code: fetchAppError?.code ?? "error", - detail: fetchAppError?.message ?? "Error fetching app.", - attribute: fetchAppError?.attribute ?? "app_id", - req, - app_id, - }); - } - - if (!validateUrl(redirect_uri, app.is_staging)) { - return errorResponse({ - statusCode: 400, - code: "invalid_redirect_uri", - detail: "Invalid redirect_uri provided.", - attribute: "redirect_uri", - req, - app_id, - }); - } - - if (app.registered_redirect_uri !== redirect_uri) { - return errorResponse({ - statusCode: 400, - code: "invalid_redirect_uri", - detail: "Invalid redirect_uri provided.", - attribute: "redirect_uri", - req, - app_id, - }); - } - - return NextResponse.json({ app_id, redirect_uri }); -} diff --git a/web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/layout.tsx b/web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/layout.tsx deleted file mode 100644 index a574768d6..000000000 --- a/web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/layout.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { SignInWithWorldIdLayout } from "@/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/layout"; -export default SignInWithWorldIdLayout; diff --git a/web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/page.tsx b/web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/page.tsx deleted file mode 100644 index 722e42d24..000000000 --- a/web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { generateMetaTitle } from "@/lib/genarate-title"; -import { SignInWithWorldIdPage } from "@/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page"; -import { Metadata } from "next"; - -export const metadata: Metadata = { - title: generateMetaTitle({ left: "Sign in with World ID" }), -}; - -export default SignInWithWorldIdPage; diff --git a/web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/proof-debugging/page.tsx b/web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/proof-debugging/page.tsx deleted file mode 100644 index f803854f7..000000000 --- a/web/app/(portal)/teams/[teamId]/apps/[appId]/sign-in-with-world-id/proof-debugging/page.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { SignInWithWorldIdProofDebuggingPage } from "@/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/ProofDebugging/page"; -export default SignInWithWorldIdProofDebuggingPage; diff --git a/web/app/api/%5Fdelete-jwks/route.ts b/web/app/api/%5Fdelete-jwks/route.ts deleted file mode 100644 index 57964ae74..000000000 --- a/web/app/api/%5Fdelete-jwks/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { POST } from "@/api/_delete-jwks"; diff --git a/web/app/api/hasura/reset-client-secret/route.ts b/web/app/api/hasura/reset-client-secret/route.ts deleted file mode 100644 index d2cf1a07f..000000000 --- a/web/app/api/hasura/reset-client-secret/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { POST } from "@/api/hasura/reset-client-secret"; diff --git a/web/app/api/v1/jwks/route.ts b/web/app/api/v1/jwks/route.ts deleted file mode 100644 index 243e17c1b..000000000 --- a/web/app/api/v1/jwks/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { GET, OPTIONS } from "@/api/v1/jwks"; diff --git a/web/app/api/v1/oidc/authorize/route.ts b/web/app/api/v1/oidc/authorize/route.ts deleted file mode 100644 index 81d47643c..000000000 --- a/web/app/api/v1/oidc/authorize/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { OPTIONS, POST } from "@/api/v1/oidc/authorize"; diff --git a/web/app/api/v1/oidc/introspect/route.ts b/web/app/api/v1/oidc/introspect/route.ts deleted file mode 100644 index d53cf057c..000000000 --- a/web/app/api/v1/oidc/introspect/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { POST } from "@/api/v1/oidc/introspect"; diff --git a/web/app/api/v1/oidc/openid-configuration/route.ts b/web/app/api/v1/oidc/openid-configuration/route.ts deleted file mode 100644 index 3f619371b..000000000 --- a/web/app/api/v1/oidc/openid-configuration/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { GET, OPTIONS } from "@/api/v1/oidc/openid-configuration"; diff --git a/web/app/api/v1/oidc/token/route.ts b/web/app/api/v1/oidc/token/route.ts deleted file mode 100644 index 5dfb64417..000000000 --- a/web/app/api/v1/oidc/token/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { OPTIONS, POST } from "@/api/v1/oidc/token"; diff --git a/web/app/api/v1/oidc/userinfo/route.ts b/web/app/api/v1/oidc/userinfo/route.ts deleted file mode 100644 index af5c26546..000000000 --- a/web/app/api/v1/oidc/userinfo/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { GET, OPTIONS, POST } from "@/api/v1/oidc/userinfo"; diff --git a/web/app/api/v1/oidc/validate/route.ts b/web/app/api/v1/oidc/validate/route.ts deleted file mode 100644 index eae4d7f0c..000000000 --- a/web/app/api/v1/oidc/validate/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { POST } from "@/api/v1/oidc/validate"; diff --git a/web/lib/constants.ts b/web/lib/constants.ts index 2c64134b8..3e0da597e 100644 --- a/web/lib/constants.ts +++ b/web/lib/constants.ts @@ -34,15 +34,9 @@ export const SECURE_DOCUMENT_SEQUENCER_STAGING = export const FACE_SEQUENCER_STAGING = "https://signup-face.stage-crypto.worldcoin.dev"; -// ANCHOR: OIDC Base URL -export const OIDC_BASE_URL = "https://id.worldcoin.org"; export const DOCS_URL = "https://docs.world.org"; export const DOCS_CLOUD_URL = "https://docs.world.org/id/cloud"; -// ANCHOR: JWKs -export const JWK_TIME_TO_LIVE = 30; // days; duration before a JWK is rotated -export const JWK_TTL_USABLE = 7; // days; duration before a JWK is rotated - export const SIMULATOR_URL = "https://simulator.worldcoin.org"; export const TELEGRAM_DEVELOPERS_GROUP_URL = "https://t.me/worldcoindevelopers"; export const TELEGRAM_MATEO_URL = "https://t.me/MateoSauton"; diff --git a/web/lib/urls.ts b/web/lib/urls.ts index 207919b46..bc2653c5a 100644 --- a/web/lib/urls.ts +++ b/web/lib/urls.ts @@ -68,9 +68,6 @@ export const urls = { createTeam: (): "/create-team" => "/create-team", - signInWorldId: (params: { team_id: string; app_id?: string }): string => - `/teams/${params.team_id}/apps/${params.app_id}/sign-in-with-world-id`, - signUp: (): "/signup" => "/signup", login: (params?: { invite_id: string }): string => diff --git a/web/next.config.mjs b/web/next.config.mjs index 7ef8150ed..0a320dd7c 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -76,10 +76,6 @@ const nextConfig = { async rewrites() { return [ - { - source: "/.well-known/openid-configuration", - destination: "/api/v1/oidc/openid-configuration", - }, { source: "/ingest/:path*", destination: "https://app.posthog.com/:path*", diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/ProofDebugging/page/index.tsx b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/ProofDebugging/page/index.tsx deleted file mode 100644 index 20fa1a7d6..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/ProofDebugging/page/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const SignInWithWorldIdProofDebuggingPage = () => { - return ( -
-

SignInWithWorldIdProofDebugging

-
- ); -}; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/error/error.tsx b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/error/error.tsx deleted file mode 100644 index b885107d3..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/error/error.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; // Error components must be Client Components - -import { useEffect } from "react"; - -export default function Error({ - error, - reset, -}: { - error: Error & { digest?: string }; - reset: () => void; -}) { - useEffect(() => { - console.error("Sign in With World ID Error: ", error); - }, [error]); - - return ( -
-

Something went wrong!

- -
- ); -} diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/layout/index.tsx b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/layout/index.tsx deleted file mode 100644 index 3b66d54da..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/layout/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SizingWrapper } from "@/components/SizingWrapper"; -import { ReactNode } from "react"; - -export const SignInWithWorldIdLayout = (props: { children: ReactNode }) => { - return ( -
- {props.children} -
- ); -}; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Links/index.tsx b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Links/index.tsx deleted file mode 100644 index 6db77024b..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Links/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { DecoratedButton } from "@/components/DecoratedButton"; -import { Input } from "@/components/Input"; -import { TYPOGRAPHY, Typography } from "@/components/Typography"; -import { yupResolver } from "@hookform/resolvers/yup"; -import clsx from "clsx"; -import { memo, useCallback } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "react-toastify"; -import * as yup from "yup"; -import { SignInActionQuery } from "../../graphql/server/fetch-signin.generated"; -import { useUpdateSignInActionMutation } from "../graphql/client/update-sign-in-action.generated"; - -const schema = yup - .object({ - privacy_policy_uri: yup.string().url("Must be a valid URL").optional(), - terms_uri: yup.string().url("Must be a valid URL").optional(), - }) - .noUnknown(); - -type ClientInformation = yup.InferType; - -// This component will not be rendered if signInAction is not defined -export const LinksForm = memo(function LinksForm(props: { - teamId: string; - signInAction: SignInActionQuery["action"][0]; - canEdit: boolean; -}) { - const { teamId, signInAction, canEdit } = props; - const [updateSignInActionMutation] = useUpdateSignInActionMutation(); - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: yupResolver(schema), - shouldFocusError: false, - defaultValues: { - privacy_policy_uri: signInAction?.privacy_policy_uri ?? "", - terms_uri: signInAction?.terms_uri ?? "", - }, - }); - - const submit = useCallback( - async (data: ClientInformation) => { - try { - if (!signInAction) return; // This should never happen - await updateSignInActionMutation({ - variables: { - id: signInAction?.id, - input: { - privacy_policy_uri: data.privacy_policy_uri, - terms_uri: data.terms_uri, - }, - }, - }); - toast.success("Links saved!"); - } catch (error) { - console.error("Update Sign in Links Error: ", error); - toast.error("Error updating action"); - } - }, - [signInAction, updateSignInActionMutation], - ); - - return ( -
-
- - Legal links - - - Links to where your Privacy Policy and Terms of Use are posted - -
- - - - - Save Changes - -
- ); -}); diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/RedirectInput/index.tsx b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/RedirectInput/index.tsx deleted file mode 100644 index 786617c47..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/RedirectInput/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"use client"; -import { validateUrl } from "@/lib/utils"; -import { yupResolver } from "@hookform/resolvers/yup"; -import clsx from "clsx"; -import { InputHTMLAttributes, memo, useMemo } from "react"; -import { useForm } from "react-hook-form"; -import { twMerge } from "tailwind-merge"; -import * as yup from "yup"; - -interface InputInterface extends InputHTMLAttributes { - required?: boolean; - currentValue?: string; - placeholder?: string; - helperText?: string; - addOnRight?: React.ReactElement; - className?: string; - isStaging: boolean; - handleChange: (value: string) => void; -} - -export const RedirectInput = memo(function Input(props: InputInterface) { - const { - required, - currentValue, - helperText, - placeholder, - className, - addOnRight, - disabled, - isStaging, - handleChange, - } = props; - - const schema = useMemo( - () => - yup - .object({ - url: yup - .string() - .required("A valid url is required") - .test("is-url", "Must be a valid URL", (value) => { - return value != null ? validateUrl(value, isStaging) : true; - }), - }) - .noUnknown(), - [isStaging], - ); - - type UrlFormValues = yup.InferType; - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: yupResolver(schema), - mode: "onChange", - shouldFocusError: false, - defaultValues: { - url: currentValue ?? "", - }, - }); - - const parentClassNames = clsx( - "rounded-lg border-[1px] bg-grey-0 px-2 text-sm text-grey-700", - { - "border-grey-200 focus-within:border-blue-500 focus-within:hover:border-blue-500 hover:border-grey-700 ": - !errors.url && !disabled, - "border-system-error-500 text-system-error-500 focus-within:border-system-error-500": - errors.url && !disabled, - }, - { - "hover:text-grey-700": !disabled, - "bg-grey-50 text-grey-300 border-grey-200": disabled, - }, - ); - - const inputClassNames = clsx( - "peer size-full bg-transparent p-2 focus:outline-none focus:ring-0", - { - "placeholder:text-grey-400": !errors.url, - "group-hover:placeholder:text-grey-700 group-hover:focus:placeholder:text-grey-400 ": - !disabled, - }, - ); - - const handleSave = handleSubmit((data) => { - handleChange(data.url); - }); - - return ( -
-
- -
{addOnRight && addOnRight}
-
-
- {helperText && ( -

{helperText}

- )} - {errors?.url?.message && ( -

- {errors.url.message} -

- )} -
-
- ); -}); diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/delete-redirect.generated.ts b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/delete-redirect.generated.ts deleted file mode 100644 index d48d5e27b..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/delete-redirect.generated.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint-disable */ -import * as Types from "@/graphql/graphql"; - -import { gql } from "@apollo/client"; -import * as Apollo from "@apollo/client"; -const defaultOptions = {} as const; -export type DeleteRedirectMutationVariables = Types.Exact<{ - id: Types.Scalars["String"]["input"]; -}>; - -export type DeleteRedirectMutation = { - __typename?: "mutation_root"; - delete_redirect_by_pk?: { __typename?: "redirect"; id: string } | null; -}; - -export const DeleteRedirectDocument = gql` - mutation DeleteRedirect($id: String!) { - delete_redirect_by_pk(id: $id) { - id - } - } -`; -export type DeleteRedirectMutationFn = Apollo.MutationFunction< - DeleteRedirectMutation, - DeleteRedirectMutationVariables ->; - -/** - * __useDeleteRedirectMutation__ - * - * To run a mutation, you first call `useDeleteRedirectMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useDeleteRedirectMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [deleteRedirectMutation, { data, loading, error }] = useDeleteRedirectMutation({ - * variables: { - * id: // value for 'id' - * }, - * }); - */ -export function useDeleteRedirectMutation( - baseOptions?: Apollo.MutationHookOptions< - DeleteRedirectMutation, - DeleteRedirectMutationVariables - >, -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation< - DeleteRedirectMutation, - DeleteRedirectMutationVariables - >(DeleteRedirectDocument, options); -} -export type DeleteRedirectMutationHookResult = ReturnType< - typeof useDeleteRedirectMutation ->; -export type DeleteRedirectMutationResult = - Apollo.MutationResult; -export type DeleteRedirectMutationOptions = Apollo.BaseMutationOptions< - DeleteRedirectMutation, - DeleteRedirectMutationVariables ->; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/delete-redirect.graphql b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/delete-redirect.graphql deleted file mode 100644 index 99a6667a5..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/delete-redirect.graphql +++ /dev/null @@ -1,5 +0,0 @@ -mutation DeleteRedirect($id: String!) { - delete_redirect_by_pk(id: $id) { - id - } -} diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/fetch-redirect.generated.ts b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/fetch-redirect.generated.ts deleted file mode 100644 index aa2b3dcb9..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/fetch-redirect.generated.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* eslint-disable */ -import * as Types from "@/graphql/graphql"; - -import { gql } from "@apollo/client"; -import * as Apollo from "@apollo/client"; -const defaultOptions = {} as const; -export type RedirectsQueryVariables = Types.Exact<{ - action_id: Types.Scalars["String"]["input"]; -}>; - -export type RedirectsQuery = { - __typename?: "query_root"; - redirect: Array<{ - __typename?: "redirect"; - id: string; - action_id: string; - redirect_uri: string; - created_at: string; - updated_at: string; - }>; -}; - -export const RedirectsDocument = gql` - query Redirects($action_id: String!) { - redirect( - where: { action_id: { _eq: $action_id } } - order_by: { created_at: asc } - ) { - id - action_id - redirect_uri - created_at - updated_at - } - } -`; - -/** - * __useRedirectsQuery__ - * - * To run a query within a React component, call `useRedirectsQuery` and pass it any options that fit your needs. - * When your component renders, `useRedirectsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useRedirectsQuery({ - * variables: { - * action_id: // value for 'action_id' - * }, - * }); - */ -export function useRedirectsQuery( - baseOptions: Apollo.QueryHookOptions< - RedirectsQuery, - RedirectsQueryVariables - > & - ( - | { variables: RedirectsQueryVariables; skip?: boolean } - | { skip: boolean } - ), -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useQuery( - RedirectsDocument, - options, - ); -} -export function useRedirectsLazyQuery( - baseOptions?: Apollo.LazyQueryHookOptions< - RedirectsQuery, - RedirectsQueryVariables - >, -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useLazyQuery( - RedirectsDocument, - options, - ); -} -export function useRedirectsSuspenseQuery( - baseOptions?: - | Apollo.SkipToken - | Apollo.SuspenseQueryHookOptions, -) { - const options = - baseOptions === Apollo.skipToken - ? baseOptions - : { ...defaultOptions, ...baseOptions }; - return Apollo.useSuspenseQuery( - RedirectsDocument, - options, - ); -} -export type RedirectsQueryHookResult = ReturnType; -export type RedirectsLazyQueryHookResult = ReturnType< - typeof useRedirectsLazyQuery ->; -export type RedirectsSuspenseQueryHookResult = ReturnType< - typeof useRedirectsSuspenseQuery ->; -export type RedirectsQueryResult = Apollo.QueryResult< - RedirectsQuery, - RedirectsQueryVariables ->; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/fetch-redirect.graphql b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/fetch-redirect.graphql deleted file mode 100644 index 51919e624..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/fetch-redirect.graphql +++ /dev/null @@ -1,12 +0,0 @@ -query Redirects($action_id: String!) { - redirect( - where: { action_id: { _eq: $action_id } } - order_by: { created_at: asc } - ) { - id - action_id - redirect_uri - created_at - updated_at - } -} diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/insert-redirect.generated.ts b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/insert-redirect.generated.ts deleted file mode 100644 index 221158af9..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/insert-redirect.generated.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable */ -import * as Types from "@/graphql/graphql"; - -import { gql } from "@apollo/client"; -import * as Apollo from "@apollo/client"; -const defaultOptions = {} as const; -export type InsertRedirectMutationVariables = Types.Exact<{ - action_id: Types.Scalars["String"]["input"]; - uri: Types.Scalars["String"]["input"]; -}>; - -export type InsertRedirectMutation = { - __typename?: "mutation_root"; - insert_redirect_one?: { - __typename?: "redirect"; - id: string; - action_id: string; - redirect_uri: string; - created_at: string; - updated_at: string; - } | null; -}; - -export const InsertRedirectDocument = gql` - mutation InsertRedirect($action_id: String!, $uri: String!) { - insert_redirect_one(object: { action_id: $action_id, redirect_uri: $uri }) { - id - action_id - redirect_uri - created_at - updated_at - } - } -`; -export type InsertRedirectMutationFn = Apollo.MutationFunction< - InsertRedirectMutation, - InsertRedirectMutationVariables ->; - -/** - * __useInsertRedirectMutation__ - * - * To run a mutation, you first call `useInsertRedirectMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useInsertRedirectMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [insertRedirectMutation, { data, loading, error }] = useInsertRedirectMutation({ - * variables: { - * action_id: // value for 'action_id' - * uri: // value for 'uri' - * }, - * }); - */ -export function useInsertRedirectMutation( - baseOptions?: Apollo.MutationHookOptions< - InsertRedirectMutation, - InsertRedirectMutationVariables - >, -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation< - InsertRedirectMutation, - InsertRedirectMutationVariables - >(InsertRedirectDocument, options); -} -export type InsertRedirectMutationHookResult = ReturnType< - typeof useInsertRedirectMutation ->; -export type InsertRedirectMutationResult = - Apollo.MutationResult; -export type InsertRedirectMutationOptions = Apollo.BaseMutationOptions< - InsertRedirectMutation, - InsertRedirectMutationVariables ->; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/insert-redirect.graphql b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/insert-redirect.graphql deleted file mode 100644 index 699b20391..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/insert-redirect.graphql +++ /dev/null @@ -1,9 +0,0 @@ -mutation InsertRedirect($action_id: String!, $uri: String!) { - insert_redirect_one(object: { action_id: $action_id, redirect_uri: $uri }) { - id - action_id - redirect_uri - created_at - updated_at - } -} diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/update-redirect.generated.ts b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/update-redirect.generated.ts deleted file mode 100644 index 119c3b0a8..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/update-redirect.generated.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* eslint-disable */ -import * as Types from "@/graphql/graphql"; - -import { gql } from "@apollo/client"; -import * as Apollo from "@apollo/client"; -const defaultOptions = {} as const; -export type UpdateRedirectMutationVariables = Types.Exact<{ - id: Types.Scalars["String"]["input"]; - uri: Types.Scalars["String"]["input"]; -}>; - -export type UpdateRedirectMutation = { - __typename?: "mutation_root"; - update_redirect_by_pk?: { - __typename?: "redirect"; - id: string; - action_id: string; - redirect_uri: string; - created_at: string; - updated_at: string; - } | null; -}; - -export const UpdateRedirectDocument = gql` - mutation UpdateRedirect($id: String!, $uri: String!) { - update_redirect_by_pk( - pk_columns: { id: $id } - _set: { redirect_uri: $uri } - ) { - id - action_id - redirect_uri - created_at - updated_at - } - } -`; -export type UpdateRedirectMutationFn = Apollo.MutationFunction< - UpdateRedirectMutation, - UpdateRedirectMutationVariables ->; - -/** - * __useUpdateRedirectMutation__ - * - * To run a mutation, you first call `useUpdateRedirectMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUpdateRedirectMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [updateRedirectMutation, { data, loading, error }] = useUpdateRedirectMutation({ - * variables: { - * id: // value for 'id' - * uri: // value for 'uri' - * }, - * }); - */ -export function useUpdateRedirectMutation( - baseOptions?: Apollo.MutationHookOptions< - UpdateRedirectMutation, - UpdateRedirectMutationVariables - >, -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation< - UpdateRedirectMutation, - UpdateRedirectMutationVariables - >(UpdateRedirectDocument, options); -} -export type UpdateRedirectMutationHookResult = ReturnType< - typeof useUpdateRedirectMutation ->; -export type UpdateRedirectMutationResult = - Apollo.MutationResult; -export type UpdateRedirectMutationOptions = Apollo.BaseMutationOptions< - UpdateRedirectMutation, - UpdateRedirectMutationVariables ->; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/update-redirect.graphql b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/update-redirect.graphql deleted file mode 100644 index 50cc95ec5..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/graphql/client/update-redirect.graphql +++ /dev/null @@ -1,9 +0,0 @@ -mutation UpdateRedirect($id: String!, $uri: String!) { - update_redirect_by_pk(pk_columns: { id: $id }, _set: { redirect_uri: $uri }) { - id - action_id - redirect_uri - created_at - updated_at - } -} diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/index.tsx b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/index.tsx deleted file mode 100644 index cc0b38f1e..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/Redirects/index.tsx +++ /dev/null @@ -1,181 +0,0 @@ -"use client"; - -import { Button } from "@/components/Button"; -import { DecoratedButton } from "@/components/DecoratedButton"; -import { CloseIcon } from "@/components/Icons/CloseIcon"; -import { TYPOGRAPHY, Typography } from "@/components/Typography"; -import clsx from "clsx"; -import posthog from "posthog-js"; -import { memo, useCallback, useState } from "react"; -import { toast } from "react-toastify"; -import { RedirectInput } from "./RedirectInput"; -import { useDeleteRedirectMutation } from "./graphql/client/delete-redirect.generated"; -import { - RedirectsDocument, - useRedirectsQuery, -} from "./graphql/client/fetch-redirect.generated"; -import { useInsertRedirectMutation } from "./graphql/client/insert-redirect.generated"; -import { useUpdateRedirectMutation } from "./graphql/client/update-redirect.generated"; - -export const Redirects = memo(function Redirects(props: { - actionId: string; - appId: string; - isStaging: boolean; - teamId: string; - canEdit: boolean; -}) { - const { actionId, appId, isStaging, teamId, canEdit } = props; - const [addRedirectFormShown, setAddRedirectFormShown] = useState(false); - - const { data, loading } = useRedirectsQuery({ - variables: { - action_id: actionId ?? "", - }, - }); - - const [insertRedirectMutation] = useInsertRedirectMutation(); - const [updateRedirectMutation] = useUpdateRedirectMutation(); - const [deleteRedirectMutation] = useDeleteRedirectMutation(); - - const addRedirect = useCallback( - async (redirect_uri: string) => { - try { - await insertRedirectMutation({ - variables: { - action_id: actionId, - uri: redirect_uri, - }, - - refetchQueries: [ - { - query: RedirectsDocument, - variables: { action_id: actionId }, - }, - ], - - awaitRefetchQueries: true, - }); - - setAddRedirectFormShown(false); - toast.success("Redirect added!"); - - posthog.capture("redirect_added_success", { - team_id: teamId, - app_id: appId, - }); - } catch (error) { - posthog.capture("redirect_add_failed", { - team_id: teamId, - app_id: appId, - }); - - console.error("Sign in redirects error: ", error); - toast.error("Error adding redirect"); - } - }, - [actionId, appId, insertRedirectMutation, teamId], - ); - - const deleteRedirect = useCallback( - async (id: string) => { - try { - await deleteRedirectMutation({ - variables: { - id, - }, - refetchQueries: [ - { - query: RedirectsDocument, - variables: { action_id: actionId }, - }, - ], - awaitRefetchQueries: true, - }); - toast.success("Redirect deleted!"); - } catch (error) { - console.error("Delete redirect error: ", error); - toast.error("Error deleting redirect"); - } - }, - [actionId, deleteRedirectMutation], - ); - - const redirects = data?.redirect; - - if (loading) return
; - - return ( -
- {redirects?.map((redirect) => ( - deleteRedirect(redirect.id)} - > - - - } - handleChange={(value: string) => { - if (value !== redirect.redirect_uri) { - updateRedirectMutation({ - variables: { id: redirect.id, uri: value }, - - refetchQueries: [ - { - query: RedirectsDocument, - variables: { action_id: actionId }, - }, - ], - - awaitRefetchQueries: true, - - onCompleted: () => { - toast.success("Redirect updated!"); - }, - }); - } - }} - /> - ))} - {addRedirectFormShown && ( - setAddRedirectFormShown(false)} - > - - - } - handleChange={async (value) => { - await addRedirect(value); - }} - /> - )} - setAddRedirectFormShown(true)} - > - Add another - -
- ); -}); diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/fetch-sign-in-action.generated.ts b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/fetch-sign-in-action.generated.ts deleted file mode 100644 index 89269062a..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/fetch-sign-in-action.generated.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable */ -import * as Types from "@/graphql/graphql"; - -import { gql } from "@apollo/client"; -import * as Apollo from "@apollo/client"; -const defaultOptions = {} as const; -export type FetchSignInActionQueryVariables = Types.Exact<{ - app_id: Types.Scalars["String"]["input"]; -}>; - -export type FetchSignInActionQuery = { - __typename?: "query_root"; - action: Array<{ - __typename?: "action"; - id: string; - app_id: string; - status: string; - privacy_policy_uri?: string | null; - terms_uri?: string | null; - }>; - app: Array<{ __typename?: "app"; is_staging: boolean; created_at: string }>; -}; - -export const FetchSignInActionDocument = gql` - query FetchSignInAction($app_id: String!) { - action(where: { app_id: { _eq: $app_id }, action: { _eq: "" } }) { - id - app_id - status - privacy_policy_uri - terms_uri - } - app(where: { id: { _eq: $app_id } }) { - is_staging - created_at - } - } -`; - -/** - * __useFetchSignInActionQuery__ - * - * To run a query within a React component, call `useFetchSignInActionQuery` and pass it any options that fit your needs. - * When your component renders, `useFetchSignInActionQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useFetchSignInActionQuery({ - * variables: { - * app_id: // value for 'app_id' - * }, - * }); - */ -export function useFetchSignInActionQuery( - baseOptions: Apollo.QueryHookOptions< - FetchSignInActionQuery, - FetchSignInActionQueryVariables - > & - ( - | { variables: FetchSignInActionQueryVariables; skip?: boolean } - | { skip: boolean } - ), -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useQuery< - FetchSignInActionQuery, - FetchSignInActionQueryVariables - >(FetchSignInActionDocument, options); -} -export function useFetchSignInActionLazyQuery( - baseOptions?: Apollo.LazyQueryHookOptions< - FetchSignInActionQuery, - FetchSignInActionQueryVariables - >, -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useLazyQuery< - FetchSignInActionQuery, - FetchSignInActionQueryVariables - >(FetchSignInActionDocument, options); -} -export function useFetchSignInActionSuspenseQuery( - baseOptions?: - | Apollo.SkipToken - | Apollo.SuspenseQueryHookOptions< - FetchSignInActionQuery, - FetchSignInActionQueryVariables - >, -) { - const options = - baseOptions === Apollo.skipToken - ? baseOptions - : { ...defaultOptions, ...baseOptions }; - return Apollo.useSuspenseQuery< - FetchSignInActionQuery, - FetchSignInActionQueryVariables - >(FetchSignInActionDocument, options); -} -export type FetchSignInActionQueryHookResult = ReturnType< - typeof useFetchSignInActionQuery ->; -export type FetchSignInActionLazyQueryHookResult = ReturnType< - typeof useFetchSignInActionLazyQuery ->; -export type FetchSignInActionSuspenseQueryHookResult = ReturnType< - typeof useFetchSignInActionSuspenseQuery ->; -export type FetchSignInActionQueryResult = Apollo.QueryResult< - FetchSignInActionQuery, - FetchSignInActionQueryVariables ->; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/fetch-sign-in-action.graphql b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/fetch-sign-in-action.graphql deleted file mode 100644 index 839206e76..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/fetch-sign-in-action.graphql +++ /dev/null @@ -1,13 +0,0 @@ -query FetchSignInAction($app_id: String!) { - action(where: { app_id: { _eq: $app_id }, action: { _eq: "" } }) { - id - app_id - status - privacy_policy_uri - terms_uri - } - app(where: { id: { _eq: $app_id } }) { - is_staging - created_at - } -} diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/reset-secret.generated.ts b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/reset-secret.generated.ts deleted file mode 100644 index 7b2b9a3fa..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/reset-secret.generated.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-disable */ -import * as Types from "@/graphql/graphql"; - -import { gql } from "@apollo/client"; -import * as Apollo from "@apollo/client"; -const defaultOptions = {} as const; -export type ResetClientSecretMutationVariables = Types.Exact<{ - app_id: Types.Scalars["String"]["input"]; - team_id: Types.Scalars["String"]["input"]; -}>; - -export type ResetClientSecretMutation = { - __typename?: "mutation_root"; - reset_client_secret?: { - __typename?: "ResetClientOutput"; - client_secret: string; - } | null; -}; - -export const ResetClientSecretDocument = gql` - mutation ResetClientSecret($app_id: String!, $team_id: String!) { - reset_client_secret(app_id: $app_id, team_id: $team_id) { - client_secret - } - } -`; -export type ResetClientSecretMutationFn = Apollo.MutationFunction< - ResetClientSecretMutation, - ResetClientSecretMutationVariables ->; - -/** - * __useResetClientSecretMutation__ - * - * To run a mutation, you first call `useResetClientSecretMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useResetClientSecretMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [resetClientSecretMutation, { data, loading, error }] = useResetClientSecretMutation({ - * variables: { - * app_id: // value for 'app_id' - * team_id: // value for 'team_id' - * }, - * }); - */ -export function useResetClientSecretMutation( - baseOptions?: Apollo.MutationHookOptions< - ResetClientSecretMutation, - ResetClientSecretMutationVariables - >, -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation< - ResetClientSecretMutation, - ResetClientSecretMutationVariables - >(ResetClientSecretDocument, options); -} -export type ResetClientSecretMutationHookResult = ReturnType< - typeof useResetClientSecretMutation ->; -export type ResetClientSecretMutationResult = - Apollo.MutationResult; -export type ResetClientSecretMutationOptions = Apollo.BaseMutationOptions< - ResetClientSecretMutation, - ResetClientSecretMutationVariables ->; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/reset-secret.graphql b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/reset-secret.graphql deleted file mode 100644 index 901b28302..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/reset-secret.graphql +++ /dev/null @@ -1,5 +0,0 @@ -mutation ResetClientSecret($app_id: String!, $team_id: String!) { - reset_client_secret(app_id: $app_id, team_id: $team_id) { - client_secret - } -} diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/update-sign-in-action.generated.ts b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/update-sign-in-action.generated.ts deleted file mode 100644 index 40f0eb520..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/update-sign-in-action.generated.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* eslint-disable */ -import * as Types from "@/graphql/graphql"; - -import { gql } from "@apollo/client"; -import * as Apollo from "@apollo/client"; -const defaultOptions = {} as const; -export type UpdateSignInActionMutationVariables = Types.Exact<{ - id: Types.Scalars["String"]["input"]; - input?: Types.InputMaybe; -}>; - -export type UpdateSignInActionMutation = { - __typename?: "mutation_root"; - update_action_by_pk?: { __typename?: "action"; id: string } | null; -}; - -export const UpdateSignInActionDocument = gql` - mutation UpdateSignInAction($id: String!, $input: action_set_input) { - update_action_by_pk(pk_columns: { id: $id }, _set: $input) { - id - } - } -`; -export type UpdateSignInActionMutationFn = Apollo.MutationFunction< - UpdateSignInActionMutation, - UpdateSignInActionMutationVariables ->; - -/** - * __useUpdateSignInActionMutation__ - * - * To run a mutation, you first call `useUpdateSignInActionMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUpdateSignInActionMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [updateSignInActionMutation, { data, loading, error }] = useUpdateSignInActionMutation({ - * variables: { - * id: // value for 'id' - * input: // value for 'input' - * }, - * }); - */ -export function useUpdateSignInActionMutation( - baseOptions?: Apollo.MutationHookOptions< - UpdateSignInActionMutation, - UpdateSignInActionMutationVariables - >, -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation< - UpdateSignInActionMutation, - UpdateSignInActionMutationVariables - >(UpdateSignInActionDocument, options); -} -export type UpdateSignInActionMutationHookResult = ReturnType< - typeof useUpdateSignInActionMutation ->; -export type UpdateSignInActionMutationResult = - Apollo.MutationResult; -export type UpdateSignInActionMutationOptions = Apollo.BaseMutationOptions< - UpdateSignInActionMutation, - UpdateSignInActionMutationVariables ->; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/update-sign-in-action.graphql b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/update-sign-in-action.graphql deleted file mode 100644 index 43d4075c0..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/graphql/client/update-sign-in-action.graphql +++ /dev/null @@ -1,5 +0,0 @@ -mutation UpdateSignInAction($id: String!, $input: action_set_input) { - update_action_by_pk(pk_columns: { id: $id }, _set: $input) { - id - } -} diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/index.tsx b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/index.tsx deleted file mode 100644 index ab38fbe76..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/ClientInformation/index.tsx +++ /dev/null @@ -1,198 +0,0 @@ -"use client"; -import { CopyButton } from "@/components/CopyButton"; -import { DecoratedButton } from "@/components/DecoratedButton"; -import { LockIcon } from "@/components/Icons/LockIcon"; -import { Input } from "@/components/Input"; -import { TYPOGRAPHY, Typography } from "@/components/Typography"; -import { Role_Enum } from "@/graphql/graphql"; -import { ORB_APP_TEAM_ID } from "@/lib/constants"; -import { Auth0SessionUser } from "@/lib/types"; -import { checkUserPermissions } from "@/lib/utils"; -import { useUser } from "@auth0/nextjs-auth0/client"; -import clsx from "clsx"; -import { ErrorPage } from "@/components/ErrorPage"; -import { SizingWrapper } from "@/components/SizingWrapper"; -import { useCallback, useMemo, useState } from "react"; -import Skeleton from "react-loading-skeleton"; -import { toast } from "react-toastify"; -import { LinksForm } from "./Links"; -import { Redirects } from "./Redirects"; -import { useFetchSignInActionQuery } from "./graphql/client/fetch-sign-in-action.generated"; -import { useResetClientSecretMutation } from "./graphql/client/reset-secret.generated"; - -export const ClientInformationPage = (props: { - appID: string; - teamID: string; -}) => { - const { appID, teamID } = props; - const [clientSecret, setClientSecret] = useState(""); - const { user } = useUser() as Auth0SessionUser; - - const isEnoughPermissions = useMemo(() => { - return checkUserPermissions(user, teamID ?? "", [ - Role_Enum.Owner, - Role_Enum.Admin, - ]); - }, [user, teamID]); - - const { data, loading: fetchingAction } = useFetchSignInActionQuery({ - variables: { app_id: appID }, - }); - - const signInAction = data?.action[0]; - const isStaging = data?.app[0]?.is_staging; - const createdAt = data?.app[0]?.created_at; - - // Check if app was created after September 29, 2025 - const isAppCreatedAfterCutoff = useMemo(() => { - if (teamID === ORB_APP_TEAM_ID) return false; - if (!createdAt) return false; - const cutoffDate = new Date("2025-09-29T00:00:00Z"); - const appCreatedDate = new Date(createdAt); - return appCreatedDate > cutoffDate; - }, [createdAt, teamID]); - - const [resetClientSecretMutation] = useResetClientSecretMutation({ - variables: { app_id: appID, team_id: teamID }, - }); - - const handleReset = useCallback(async () => { - try { - const result = await resetClientSecretMutation(); - - if (result instanceof Error) { - throw result; - } - - setClientSecret(result.data?.reset_client_secret?.client_secret ?? ""); - toast.success("Client secret reset"); - } catch (error) { - console.error("Reset Client Secret Error: ", error); - toast.error("Failed to reset client secret"); - } - }, [resetClientSecretMutation]); - - if (fetchingAction) { - return ( -
- -
- ); - } - - if (isAppCreatedAfterCutoff) { - return ( -
-
- Feature Not Available - - Your app was created after Sign in with World ID was deprecated and - is not eligible for this feature. Please read the announcement - above. - -
-
- ); - } - - if (!fetchingAction && !signInAction) { - return ( - - - - ); - } - - return ( -
-
-
- Client information - - - Use these attributes to configure Sign in with World ID in your app - -
- -
- } - /> - - - ) : ( - <> - ) - } - addOnRight={ -
- - Reset - - - {clientSecret !== "" && ( - - )} -
- } - /> -
-
- -
-
- - Redirects - - - - You must specify at least one URL for authentication to work - -
- - -
- - -
- ); -}; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/graphql/server/fetch-signin.generated.ts b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/graphql/server/fetch-signin.generated.ts deleted file mode 100644 index 48ef7add3..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/graphql/server/fetch-signin.generated.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable */ -import * as Types from "@/graphql/graphql"; - -import { GraphQLClient, RequestOptions } from "graphql-request"; -import gql from "graphql-tag"; -type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; -export type SignInActionQueryVariables = Types.Exact<{ - app_id: Types.Scalars["String"]["input"]; -}>; - -export type SignInActionQuery = { - __typename?: "query_root"; - action: Array<{ - __typename?: "action"; - id: string; - app_id: string; - status: string; - privacy_policy_uri?: string | null; - terms_uri?: string | null; - }>; -}; - -export const SignInActionDocument = gql` - query SignInAction($app_id: String!) { - action(where: { app_id: { _eq: $app_id }, action: { _eq: "" } }) { - id - app_id - status - privacy_policy_uri - terms_uri - } - } -`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string, - variables?: any, -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType, - _variables, -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper, -) { - return { - SignInAction( - variables: SignInActionQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders, - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request(SignInActionDocument, variables, { - ...requestHeaders, - ...wrappedRequestHeaders, - }), - "SignInAction", - "query", - variables, - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/graphql/server/fetch-signin.graphql b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/graphql/server/fetch-signin.graphql deleted file mode 100644 index 3c86c725d..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/graphql/server/fetch-signin.graphql +++ /dev/null @@ -1,9 +0,0 @@ -query SignInAction($app_id: String!) { - action(where: { app_id: { _eq: $app_id }, action: { _eq: "" } }) { - id - app_id - status - privacy_policy_uri - terms_uri - } -} diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/index.tsx b/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/index.tsx deleted file mode 100644 index 3e77fc655..000000000 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/SignInWithWorldId/page/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { DecoratedButton } from "@/components/DecoratedButton"; -import { DocsIcon } from "@/components/Icons/DocsIcon"; -import { GithubIcon } from "@/components/Icons/GithubIcon"; -import { WarningErrorIcon } from "@/components/Icons/WarningErrorIcon"; -import { TYPOGRAPHY, Typography } from "@/components/Typography"; -import Image from "next/image"; -import { ClientInformationPage } from "./ClientInformation"; - -type SignInWithWorldIdPageProps = { - params: Record | null | undefined; -}; -export const SignInWithWorldIdPage = async ( - props: SignInWithWorldIdPageProps, -) => { - const { params } = props; - const appId = params?.appId as `app_${string}`; - const teamId = params?.teamId as string; - - return ( -
-
- passport -
- Sign in with World ID - - Let users sign in to your app with their World ID using OpenID - Connect (OIDC) - -
-
- - - - See an example - - - - - - Learn more - - -
-
- - -
- -
-
-
- Sign in with World ID is sunsetting in{" "} - December 2025. -
-
- New apps created after September 29, 2025 cannot enable this - feature. -
-
- Read the full announcement → -
-
-
-
-
- -
-
- -
-
- ); -}; diff --git a/web/scenes/Portal/Teams/TeamId/Apps/AppId/layout/AppIdChrome.tsx b/web/scenes/Portal/Teams/TeamId/Apps/AppId/layout/AppIdChrome.tsx index 88b09f6b7..b6746af76 100644 --- a/web/scenes/Portal/Teams/TeamId/Apps/AppId/layout/AppIdChrome.tsx +++ b/web/scenes/Portal/Teams/TeamId/Apps/AppId/layout/AppIdChrome.tsx @@ -5,7 +5,6 @@ import { AppIcon } from "@/components/Icons/AppIcon"; import { DashboardSquareIcon } from "@/components/Icons/DashboardSquareIcon"; import { IncognitoIcon } from "@/components/Icons/IncognitoIcon"; import { TransactionIcon } from "@/components/Icons/TransactionIcon"; -import { UserAccountIcon } from "@/components/Icons/UserAccountIcon"; import { SizingWrapper } from "@/components/SizingWrapper"; import { Tab, Tabs } from "@/components/Tabs"; import { TYPOGRAPHY, Typography } from "@/components/Typography"; @@ -228,18 +227,6 @@ export const AppIdChrome = ({ Incognito actions - {!isOnChainApp && ( - - - Sign in with World ID - - - )} - - - - - ({ - kid: "kid_my_test_key", - kms_id: "kms_my_test_id", - })), - fetchActiveJWK: jest.fn().mockImplementation(() => ({ - kid: "kid_my_test_key", - kms_id: "kms_my_test_id", - })), -}; diff --git a/web/tests/api/__mocks__/kms.mock.ts b/web/tests/api/__mocks__/kms.mock.ts index 11dd702de..e4bdb414d 100644 --- a/web/tests/api/__mocks__/kms.mock.ts +++ b/web/tests/api/__mocks__/kms.mock.ts @@ -16,7 +16,6 @@ module.exports = { }; }), })), - signJWTWithKMSKey: jest.requireActual("@/api/helpers/kms").signJWTWithKMSKey, createKMSKey: jest.fn().mockImplementation(async () => { const key = createPublicKey({ format: "jwk", key: publicJwk }); const pemKey = key.export({ type: "pkcs1", format: "pem" }); diff --git a/web/tests/api/delete-jwks.test.ts b/web/tests/api/delete-jwks.test.ts deleted file mode 100644 index 8145400d6..000000000 --- a/web/tests/api/delete-jwks.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { POST } from "@/api/_delete-jwks"; -import { logger } from "@/lib/logger"; -import { NextRequest } from "next/server"; - -let consoleInfoSpy: jest.SpyInstance; - -jest.mock("@/api/helpers/graphql", () => ({ - getAPIServiceGraphqlClient: jest.fn(), -})); -const DeleteExpiredJWKs = jest.fn(); -jest.mock("@/api/helpers/jwks/graphql/delete-expired-jwks.generated", () => ({ - getSdk: () => ({ - DeleteExpiredJWKs, - }), -})); - -beforeEach(() => { - consoleInfoSpy = jest - .spyOn(logger, "info") - .mockImplementation(async () => {}); -}); - -afterEach(() => { - consoleInfoSpy.mockRestore(); -}); - -describe("/api/v1/_delete-jwks", () => { - test("endpoint is only accessible with specific token (Hasura)", async () => { - const request = new NextRequest( - "http://localhost:3000/api/v1/_delete-jwks", - { - method: "POST", - }, - ); - - const response = await POST(request); - - expect(response?.status).toBe(403); - expect(await response?.json()).toEqual({ - code: "permission_denied", - detail: "You do not have permission to perform this action.", - attribute: null, - }); - }); - - test("will not delete jwks if none are expired", async () => { - const request = new NextRequest( - "http://localhost:3000/api/v1/_delete-jwks", - { - method: "POST", - headers: { - authorization: process.env.INTERNAL_ENDPOINTS_SECRET || "", - }, - }, - ); - - DeleteExpiredJWKs.mockResolvedValue({ - delete_jwks: { returning: [], __typename: "jwks_mutation_response" }, - }); - - const response = await POST(request); - - expect(response?.status).toBe(204); - expect(consoleInfoSpy).toHaveBeenCalledTimes(2); - expect(consoleInfoSpy).toHaveBeenNthCalledWith( - 1, - "Starting deletion of expired jwks.", - ); - expect(consoleInfoSpy).toHaveBeenNthCalledWith( - 2, - "Deleted 0 expired jwks.", - ); - }); - - test("will delete all jwks if past expiration date", async () => { - const request = new NextRequest( - "http://localhost:3000/api/v1/_delete-jwks", - { - method: "POST", - headers: { - authorization: process.env.INTERNAL_ENDPOINTS_SECRET || "", - }, - }, - ); - - DeleteExpiredJWKs.mockResolvedValue({ - delete_jwks: { - returning: [ - { - id: "jwk_4b4e07011e4766c69062b90ff384afc4", - kms_id: "f9f0ba3b-054b-4c27-bcc4-3c22c328117e", - __typename: "jwks", - }, - ], - __typename: "jwks_mutation_response", - }, - }); - - const response = await POST(request); - - expect(response?.status).toBe(204); - expect(consoleInfoSpy).toHaveBeenCalledTimes(2); - expect(consoleInfoSpy).toHaveBeenNthCalledWith( - 1, - "Starting deletion of expired jwks.", - ); - expect(consoleInfoSpy).toHaveBeenNthCalledWith( - 2, - "Deleted 1 expired jwks.", - ); - }); -}); diff --git a/web/tests/api/v1/oidc/authorize.test.ts b/web/tests/api/v1/oidc/authorize.test.ts deleted file mode 100644 index a78ea0b34..000000000 --- a/web/tests/api/v1/oidc/authorize.test.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { OIDCErrorCodes, OIDCScopes } from "@/api/helpers/oidc"; -import { POST } from "@/api/v1/oidc/authorize"; -import { OIDCResponseType } from "@/lib/types"; -import { createPublicKey } from "crypto"; -import dayjs from "dayjs"; -import { jwtVerify } from "jose"; -import { NextRequest } from "next/server"; -import { publicJwk } from "../../__mocks__/jwk"; -import { semaphoreProofParamsMock } from "../../__mocks__/proof.mock"; - -// Mock the external dependencies -jest.mock("@/api/helpers/graphql", () => ({ - getAPIServiceGraphqlClient: jest.fn(), -})); - -jest.mock("@/api/helpers/kms", () => - require("tests/api/__mocks__/kms.mock.ts"), -); - -jest.mock("@/api/helpers/jwks", () => - require("tests/api/__mocks__/jwks.mock.ts"), -); - -// Mock the GraphQL SDKs -const FetchOIDCApp = jest.fn(); -const Nullifier = jest.fn(); -const UpsertNullifier = jest.fn(); -const InsertAuthCode = jest.fn(); - -jest.mock("@/api/helpers/oidc/graphql/fetch-oidc-app.generated", () => ({ - getSdk: () => ({ - FetchOIDCApp, - }), -})); - -jest.mock("@/api/v1/oidc/authorize/graphql/fetch-nullifier.generated", () => ({ - getSdk: () => ({ - Nullifier, - }), -})); - -jest.mock("@/api/v1/oidc/authorize/graphql/upsert-nullifier.generated", () => ({ - getSdk: () => ({ - UpsertNullifier, - }), -})); - -jest.mock("@/api/helpers/oidc/graphql/insert-auth-code.generated", () => ({ - getSdk: () => ({ - InsertAuthCode, - }), -})); - -// Mock the verifyProof function -jest.mock("@/api/helpers/verify", () => ({ - verifyProof: jest.fn().mockResolvedValue({ error: null }), -})); - -beforeEach(async () => { - await global.RedisClient?.flushall(); - - // Mock OIDC app fetch - FetchOIDCApp.mockResolvedValue({ - app: [ - { - id: "app_112233445566778", - is_staging: false, - actions: [ - { - id: "action_staging_112233445566778", - action: "", - status: "active", - external_nullifier: - "0x1c75ff6366690115808bd58e4c6e3342068088703dffa0a0ee07f55892bb10bd", - redirects: [ - { - redirect_uri: "https://example.com/cb", - }, - ], - }, - ], - }, - ], - }); - - // Mock nullifier operations - Nullifier.mockResolvedValue({ nullifier: [] }); - UpsertNullifier.mockResolvedValue({ - insert_nullifier_one: { nullifier_hash: "0x123", id: "nil_123" }, - }); - - // Mock auth code insertion - InsertAuthCode.mockImplementation((args) => ({ - insert_auth_code_one: { auth_code: args.auth_code }, - })); -}); - -const VALID_REQUEST: Record = { - ...semaphoreProofParamsMock, - app_id: "app_1234", - scope: OIDCScopes.OpenID, - response_type: OIDCResponseType.Code, - redirect_uri: "https://example.com/cb", -}; - -describe("/api/v1/oidc/authorize [request validation]", () => { - test("validate required attributes", async () => { - const required_attributes = [ - "proof", - "nullifier_hash", - "merkle_root", - "verification_level", - "app_id", - "response_type", - "redirect_uri", - ]; - for (const attribute of required_attributes) { - const body = { ...VALID_REQUEST, [attribute]: undefined }; - delete body[attribute]; - const req = new NextRequest( - "http://localhost:3000/api/v1/oidc/authorize", - { - method: "POST", - body: JSON.stringify(body), - }, - ); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(400); - expect(data).toMatchObject({ - code: "validation_error", - attribute, - detail: "This attribute is required.", - }); - } - }); - - test("openid scope is always required for OIDC requests", async () => { - const invalid_scopes = ["invalid", "profile%20email", undefined, ""]; - for (const scope of invalid_scopes) { - const req = new NextRequest( - "http://localhost:3000/api/v1/oidc/authorize", - { - method: "POST", - body: JSON.stringify({ ...VALID_REQUEST, scope }), - }, - ); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(400); - expect(data).toMatchObject({ - attribute: "scope", - detail: "The openid scope is always required.", - }); - } - }); - - test("invalid response_type throws an error", async () => { - const invalid_response_types = [ - "invalid", - "code%20invalid", - "code invalid", - ]; - for (const response_type of invalid_response_types) { - const req = new NextRequest( - "http://localhost:3000/api/v1/oidc/authorize", - { - method: "POST", - body: JSON.stringify({ ...VALID_REQUEST, response_type }), - }, - ); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(400); - expect(data).toMatchObject({ - attribute: "response_type", - code: OIDCErrorCodes.UnsupportedResponseType, - }); - } - }); - - test("validate redirect_uri", async () => { - const invalid_redirect_uris = [ - "http://example.com/cb", - "https://example.com/cb?query=string", - "https://example.com", - "https://evil.com", - ]; - for (const redirect_uri of invalid_redirect_uris) { - const req = new NextRequest( - "http://localhost:3000/api/v1/oidc/authorize", - { - method: "POST", - body: JSON.stringify({ ...VALID_REQUEST, redirect_uri }), - }, - ); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(400); - expect(data).toMatchObject({ - attribute: "redirect_uri", - detail: "Invalid redirect URI.", - code: OIDCErrorCodes.InvalidRedirectURI, - }); - } - }); -}); - -describe("/api/v1/oidc/authorize [authorization code flow]", () => { - test("returns an authorization code", async () => { - const req = new NextRequest("http://localhost:3000/api/v1/oidc/authorize", { - method: "POST", - body: JSON.stringify({ ...VALID_REQUEST }), - }); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(200); - expect(data).toEqual({ - code: expect.stringMatching(/^[a-f0-9]{16,30}$/), - }); - }); - - test("prevents replayed proofs", async () => { - const req1 = new NextRequest( - "http://localhost:3000/api/v1/oidc/authorize", - { - method: "POST", - body: JSON.stringify({ ...VALID_REQUEST }), - }, - ); - - const req2 = new NextRequest( - "http://localhost:3000/api/v1/oidc/authorize", - { - method: "POST", - body: JSON.stringify({ ...VALID_REQUEST }), - }, - ); - - await POST(req1); - const response = await POST(req2); - const data = await response.json(); - - expect(response.status).toBe(400); - expect(data).toMatchObject({ - code: "invalid_proof", - attribute: "proof", - detail: "This proof has already been used. Please try again", - }); - }); -}); - -describe("/api/v1/oidc/authorize [implicit flow]", () => { - test("returns a valid token", async () => { - const req = new NextRequest("http://localhost:3000/api/v1/oidc/authorize", { - method: "POST", - body: JSON.stringify({ ...VALID_REQUEST, response_type: "id_token" }), - }); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(200); - expect(data).toEqual({ id_token: expect.any(String) }); - - const jwt = data.id_token; - const publicKey = createPublicKey({ format: "jwk", key: publicJwk }); - const { protectedHeader, payload } = await jwtVerify(jwt, publicKey); - - expect(protectedHeader).toEqual({ - alg: "RS256", - kid: "kid_my_test_key", - typ: "JWT", - }); - - expect(payload).toEqual({ - iss: "https://id.worldcoin.org", - sub: semaphoreProofParamsMock.nullifier_hash, - jti: expect.any(String), - iat: expect.any(Number), - exp: expect.any(Number), - aud: "app_112233445566778", - scope: "openid", - "https://id.worldcoin.org/beta": { - likely_human: "strong", - credential_type: "orb", - warning: - "DEPRECATED and will be removed soon. Use `https://id.worldcoin.org/v1` instead.", - }, - "https://id.worldcoin.org/v1": { - verification_level: "orb", - }, - nonce: semaphoreProofParamsMock.signal, - }); - - // Validate timestamps - const iatDiff = Math.abs(dayjs().diff(dayjs.unix(payload.iat!), "second")); - const oneHourFromNow = new Date().getTime() + 60 * 60 * 1000; - - const expDiff = Math.abs(oneHourFromNow / 1000 - payload.exp!); - expect(iatDiff).toBeLessThan(2); // 2 sec - expect(expDiff).toBeLessThan(2); // 2 sec - expect(payload.iat!.toString().length).toEqual(10); // timestamp in seconds has 10 digits - }); -}); - -describe("/api/v1/oidc/authorize [hybrid flow]", () => { - test("returns a valid token and authorization code", async () => { - const req = new NextRequest("http://localhost:3000/api/v1/oidc/authorize", { - method: "POST", - body: JSON.stringify({ - ...VALID_REQUEST, - response_type: "code id_token", - }), - }); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(200); - expect(data).toEqual({ - id_token: expect.any(String), - code: expect.stringMatching(/^[a-f0-9]{16,30}$/), - }); - - const jwt = data.id_token; - const publicKey = createPublicKey({ format: "jwk", key: publicJwk }); - const { protectedHeader, payload } = await jwtVerify(jwt, publicKey); - - expect(protectedHeader).toEqual({ - alg: "RS256", - kid: "kid_my_test_key", - typ: "JWT", - }); - - expect(payload).toEqual({ - iss: "https://id.worldcoin.org", - sub: semaphoreProofParamsMock.nullifier_hash, - jti: expect.any(String), - iat: expect.any(Number), - exp: expect.any(Number), - aud: "app_112233445566778", - scope: "openid", - "https://id.worldcoin.org/beta": { - likely_human: "strong", - credential_type: "orb", - warning: - "DEPRECATED and will be removed soon. Use `https://id.worldcoin.org/v1` instead.", - }, - "https://id.worldcoin.org/v1": { - verification_level: "orb", - }, - nonce: semaphoreProofParamsMock.signal, - }); - - // Validate timestamps - const iatDiff = Math.abs(dayjs().diff(dayjs.unix(payload.iat!), "second")); - const oneHourFromNow = new Date().getTime() + 60 * 60 * 1000; - - const expDiff = Math.abs(oneHourFromNow / 1000 - payload.exp!); - expect(iatDiff).toBeLessThan(2); // 2 sec - expect(expDiff).toBeLessThan(2); // 2 sec - expect(payload.iat!.toString().length).toEqual(10); // timestamp in seconds has 10 digits - }); -}); diff --git a/web/tests/api/v1/oidc/userinfo.test.ts b/web/tests/api/v1/oidc/userinfo.test.ts deleted file mode 100644 index 6c881f8c4..000000000 --- a/web/tests/api/v1/oidc/userinfo.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { generateOIDCJWT } from "@/api/helpers/jwts"; -import { OIDCScopes } from "@/api/helpers/oidc"; -import { VerificationLevel } from "@worldcoin/idkit-core"; - -import { POST } from "@/api/v1/oidc/userinfo"; -import { NextRequest } from "next/server"; - -jest.mock("@/api/helpers/kms", () => - require("tests/api/__mocks__/kms.mock.ts"), -); - -jest.mock("@/api/helpers/jwks", () => - require("tests/api/__mocks__/jwks.mock.ts"), -); - -describe("/api/v1/oidc/userinfo", () => { - test("invalid jwt", async () => { - const jwt = await generateOIDCJWT({ - kid: "test-key", - nonce: "1234", - app_id: "app_1234", - kms_id: "test-kms-id", - nullifier_hash: "0x00000", - verification_level: VerificationLevel.Orb, - scope: [OIDCScopes.OpenID, OIDCScopes.Profile], - }); - // Ensure we're actually generating a JWT - expect(jwt).toMatch(/^[\w-]*\.[\w-]*\.[\w-]*$/); - - const req = new NextRequest("http://localhost:3000/api/v1/oidc/userinfo", { - method: "POST", - headers: { - authorization: `Bearer ${jwt}`, - }, - }); - - const response = await POST(req); - - expect(response.status).toBe(401); - const responseBody = await response.json(); - expect(responseBody).toMatchObject({ - attribute: "token", - code: "invalid_token", - }); - }); -}); diff --git a/web/tests/api/v1/oidc/validate.test.ts b/web/tests/api/v1/oidc/validate.test.ts deleted file mode 100644 index 12de15ab2..000000000 --- a/web/tests/api/v1/oidc/validate.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { OIDCErrorCodes } from "@/api/helpers/oidc"; -import { POST } from "@/api/v1/oidc/validate"; -import { NextRequest } from "next/server"; - -jest.mock("@/api/helpers/graphql", () => ({ - getAPIServiceGraphqlClient: jest.fn(), -})); -const FetchOIDCApp = jest.fn(); -jest.mock("@/api/helpers/oidc/graphql/fetch-oidc-app.generated", () => ({ - getSdk: () => ({ - FetchOIDCApp, - }), -})); -beforeEach(() => { - FetchOIDCApp.mockResolvedValue({ - app: [ - { - id: "app_0123456789", - is_staging: true, - actions: [ - { - external_nullifier: "external_nullifier", - redirects: [ - { - redirect_uri: "https://example.com", - }, - ], - }, - ], - }, - ], - cache: [ - { - key: "staging.semaphore.wld.eth", - value: "0x000000000000000000000", - }, - ], - }); -}); - -describe("/api/v1/oidc/validate", () => { - test("can validate app and redirect_uri", async () => { - const req = new NextRequest("http://localhost:3000/api/v1/oidc/validate", { - method: "POST", - body: JSON.stringify({ - app_id: "app_0123456789", - redirect_uri: "https://example.com", - }), - }); - - const response = await POST(req); - - expect(response.status).toBe(200); - const responseBody = await response.json(); - expect(responseBody).toMatchObject({ - app_id: "app_0123456789", - redirect_uri: "https://example.com", - }); - }); - - test("invalid app_id", async () => { - const req = new NextRequest("http://localhost:3000/api/v1/oidc/validate", { - method: "POST", - body: JSON.stringify({ - app_id: "app_invalid", - redirect_uri: "https://example.com", - }), - }); - - FetchOIDCApp.mockResolvedValueOnce({ - app: [], - cache: [ - { - key: "staging.semaphore.wld.eth", - value: "0x000000000000000000000", - }, - ], - }); - - const response = await POST(req); - - expect(response.status).toBe(404); - const responseBody = await response.json(); - expect(responseBody).toMatchObject({ - attribute: "app_id", - code: "app_not_found", - }); - }); - - test("invalid redirect_uri", async () => { - const req = new NextRequest("http://localhost:3000/api/v1/oidc/validate", { - method: "POST", - body: JSON.stringify({ - app_id: "app_0123456789", - redirect_uri: "https://invalid.com", - }), - }); - - const response = await POST(req); - - expect(response.status).toBe(400); - const responseBody = await response.json(); - expect(responseBody).toMatchObject({ - code: OIDCErrorCodes.InvalidRedirectURI, - attribute: "redirect_uri", - }); - }); -}); diff --git a/web/tests/integration/action.test.ts b/web/tests/integration/action.test.ts index 8a1bd1df4..59c7229f9 100644 --- a/web/tests/integration/action.test.ts +++ b/web/tests/integration/action.test.ts @@ -29,17 +29,10 @@ describe("service role", () => { }), ); - let signInWithWorldIDCount = 0; - for (const row of response.data.action) { // Service role should not see archived actions expect(row.app_id).not.toEqual(rows[0].id); - if (row.name === "Sign in with World ID") { - signInWithWorldIDCount++; - } } - - expect(signInWithWorldIDCount).toEqual(1); // only one app with sign in with world id }); test("can query return_to fields from actions", async () => { diff --git a/web/tests/integration/app.test.ts b/web/tests/integration/app.test.ts index 56a6f3f74..587202b80 100644 --- a/web/tests/integration/app.test.ts +++ b/web/tests/integration/app.test.ts @@ -2,7 +2,6 @@ import { gql } from "@apollo/client"; import { integrationDBClean, integrationDBExecuteQuery } from "./setup"; -import { POST } from "@/api/hasura/reset-client-secret"; import { NextRequest } from "next/server"; import { getAPIClient, getAPIUserClient } from "./test-utils"; // TODO: Consider moving this to a generalized jest environment @@ -158,133 +157,6 @@ describe("user role", () => { } } }); - - test("cannot reset client secret as a member", async () => { - const { rows: teams } = (await integrationDBExecuteQuery( - `SELECT id FROM "public"."team";`, - )) as { rows: Array<{ id: string }> }; - - const { rows: teamMemberships } = (await integrationDBExecuteQuery( - `SELECT id, user_id, team_id FROM "public"."membership" WHERE "team_id" = '${teams[0].id}' AND "role" = 'MEMBER' limit 1;`, - )) as { rows: Array<{ id: string; user_id: string; team_id: string }> }; - - const { rows: teamApps } = (await integrationDBExecuteQuery( - `SELECT id FROM "public"."app" WHERE "team_id" = '${teams[0].id}' limit 1;`, - )) as { rows: Array<{ id: string }> }; - - // Test invalid role - const tokenTeamId = teams[0].id; - const tokenUserId = teamMemberships[0].user_id; - const appId = teamApps[0].id; - - const req = new NextRequest( - `${process.env.NEXT_PUBLIC_APP_URL}/api/hasura/reset-client-secret`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: process.env.INTERNAL_ENDPOINTS_SECRET!, - }, - body: JSON.stringify({ - input: { app_id: appId, team_id: tokenTeamId }, - action: { name: "reset_client_secret" }, - session_variables: { - "x-hasura-role": "user", - "x-hasura-user-id": tokenUserId, - }, - }), - }, - ); - - const res = await POST(req); - - const responseJSON = await res?.json(); - expect(responseJSON.extensions.code).toBe("insufficient_permissions"); - }); - - test("cannot reset client secret for another team", async () => { - const { rows: teams } = (await integrationDBExecuteQuery( - `SELECT id FROM "public"."team";`, - )) as { rows: Array<{ id: string }> }; - - const { rows: teamMemberships } = (await integrationDBExecuteQuery( - `SELECT id, user_id, team_id FROM "public"."membership" WHERE "team_id" = '${teams[1].id}' AND "role" = 'OWNER' limit 1;`, - )) as { rows: Array<{ id: string; user_id: string; team_id: string }> }; - - const { rows: teamApps } = (await integrationDBExecuteQuery( - `SELECT id FROM "public"."app" WHERE "team_id" = '${teams[0].id}' limit 1;`, - )) as { rows: Array<{ id: string }> }; - - const tokenTeamId = teams[1].id; - const tokenUserId = teamMemberships[0].user_id; - const appId = teamApps[0].id; - - const req = new NextRequest( - `${process.env.NEXT_PUBLIC_APP_URL}/api/hasura/reset-client-secret`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: process.env.INTERNAL_ENDPOINTS_SECRET!, - }, - body: JSON.stringify({ - input: { app_id: appId, team_id: tokenTeamId }, - action: { name: "reset_client_secret" }, - session_variables: { - "x-hasura-role": "user", - "x-hasura-user-id": tokenUserId, - }, - }), - }, - ); - - const res = await POST(req); - - const responseJSON = await res?.json(); - expect(responseJSON.extensions.code).toBe("insufficient_permissions"); - }); - - test("can reset client secret", async () => { - const { rows: teams } = (await integrationDBExecuteQuery( - `SELECT id FROM "public"."team";`, - )) as { rows: Array<{ id: string }> }; - - const { rows: teamMemberships } = (await integrationDBExecuteQuery( - `SELECT id, user_id, team_id FROM "public"."membership" WHERE "team_id" = '${teams[0].id}' AND "role" = 'OWNER' limit 1;`, - )) as { rows: Array<{ id: string; user_id: string; team_id: string }> }; - - const { rows: teamApps } = (await integrationDBExecuteQuery( - `SELECT id FROM "public"."app" WHERE "team_id" = '${teams[0].id}' limit 1;`, - )) as { rows: Array<{ id: string }> }; - - const tokenTeamId = teams[0].id; - const tokenUserId = teamMemberships[0].user_id; - const appId = teamApps[0].id; - - const req = new NextRequest( - `${process.env.NEXT_PUBLIC_APP_URL}/api/hasura/reset-client-secret`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: process.env.INTERNAL_ENDPOINTS_SECRET!, - }, - body: JSON.stringify({ - input: { app_id: appId, team_id: tokenTeamId }, - action: { name: "reset_client_secret" }, - session_variables: { - "x-hasura-role": "user", - "x-hasura-user-id": tokenUserId, - }, - }), - }, - ); - - const res = await POST(req); - - const responseJSON = await res?.json(); - expect(responseJSON.client_secret).toBeDefined(); - }); }); describe("api_key role", () => { diff --git a/web/tests/integration/jwks.test.ts b/web/tests/integration/jwks.test.ts deleted file mode 100644 index e46ab4331..000000000 --- a/web/tests/integration/jwks.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { fetchActiveJWK, generateJWK, retrieveJWK } from "@/api/helpers/jwks"; -import { createKMSKey, getKMSClient } from "@/api/helpers/kms"; -import { integrationDBClean, integrationDBExecuteQuery } from "./setup"; - -jest.mock("@/api/helpers/kms", () => { - return { - getKMSClient: jest.fn(), - createKMSKey: jest.fn(), - scheduleKeyDeletion: jest.fn(), - }; -}); - -jest.mock("@/api/helpers/kms", () => - require("tests/api/__mocks__/kms.mock.ts"), -); - -beforeEach(integrationDBClean); -describe("jwks management", () => { - it("can retrieve existing jwks", async () => { - const { rows } = await integrationDBExecuteQuery( - 'SELECT * FROM "public"."jwks" LIMIT 1;', - ); - - const jwk = await retrieveJWK(rows[0].id); - expect(jwk.kid).toEqual(rows[0].id); - expect(jwk.kms_id).toEqual(rows[0].kms_id); - }); - - it("throws error if the jwk is not found", async () => { - await expect(retrieveJWK("non-existing-jwk")).rejects.toThrowError( - "JWK not found.", - ); - }); - - it("fetches an active jwk", async () => { - const { rows } = await integrationDBExecuteQuery( - 'SELECT * FROM "public"."jwks" LIMIT 1;', - ); - - const jwk = await fetchActiveJWK(); - expect(jwk.kid).toEqual(rows[0].id); - }); - - it("does not rotate a jwk with more than 7 days to expire", async () => { - const { rows } = await integrationDBExecuteQuery( - 'SELECT * FROM "public"."jwks" WHERE "expires_at" > NOW() + INTERVAL \'7 days\' LIMIT 1;', - ); - - const jwk = await fetchActiveJWK(); - expect(jwk.kid).toEqual(rows[0].id); - }); - - it("rotates a jwk with less than 7 days to expire", async () => { - const { rows } = await integrationDBExecuteQuery( - 'UPDATE "public"."jwks" SET "expires_at" = NOW() + INTERVAL \'6 days\' WHERE "id" = (SELECT id FROM "public"."jwks" LIMIT 1) RETURNING "id";', - ); - - const jwk = await fetchActiveJWK(); - expect(jwk.kid).not.toEqual(rows[0].id); - }); - - it("can generate new kms keys", async () => { - // Mock the responses for KMS functions - (getKMSClient as jest.Mock).mockReturnValue(true); - (createKMSKey as jest.Mock).mockReturnValue({ - keyId: "da112a8b-023d-4eda-ae7d-33fde0a721b4", - publicKey: `-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvzV3R48ve50etEd4BtryHzo1x1h1tC1poHkSXGzjXPIXmYvuLyZPCWfNzuH9YpXfuZRch1p3YrFRavSoQClb/kfAOou/nZXPyFdVlhzQzLp0EGB+/WEjA5Zj4J39EDdyToXmxsVNezzZJG66kfhz1VmBd18WGGAPDvw9PAdR2LpybKXl9VvwY5CFHazkadFy8Any+nKHpn3R3MxRHaeJV3EZDJfC+C46BCULkAS8EnZAtfdTJubIE71cNoOu/WmQupYsotk1XT3aN07ctvYuhyejiE+6bU3awre/kOumyjzb/7UWeIMvwxbFor3fEUPJa70xFfqPJUpFyj8NXlPE5wIDAQAB ------END PUBLIC KEY-----`, - }); - - const result = await generateJWK(); - expect(result.keyId).toEqual("da112a8b-023d-4eda-ae7d-33fde0a721b4"); - }); - - it("throws error if kms client cannot be created", async () => { - // Mock the responses for KMS functions - (getKMSClient as jest.Mock).mockReturnValue(false); - - await expect(generateJWK()).rejects.toThrowError("KMS client not found."); - }); - - it("throws error if kms key generation fails", async () => { - // Mock the responses for KMS functions - (getKMSClient as jest.Mock).mockReturnValue(true); - (createKMSKey as jest.Mock).mockReturnValue({}); - - await expect(generateJWK()).rejects.toThrowError( - "Unable to create KMS key.", - ); - }); -}); diff --git a/web/tests/integration/oidc/authorize.test.ts b/web/tests/integration/oidc/authorize.test.ts deleted file mode 100644 index d7a89f7b2..000000000 --- a/web/tests/integration/oidc/authorize.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { OIDCErrorCodes } from "@/api/helpers/oidc"; -import { POST } from "@/api/v1/oidc/authorize"; -import { createHash } from "crypto"; -import { NextRequest } from "next/server"; -import { semaphoreProofParamsMock } from "tests/api/__mocks__/proof.mock"; -import { integrationDBClean, integrationDBExecuteQuery } from "../setup"; -import { testGetDefaultApp } from "../test-utils"; - -// Mock the verifyProof function -jest.mock("@/api/helpers/verify", () => ({ - verifyProof: jest.fn().mockResolvedValue({ error: null }), -})); - -beforeEach(async () => { - await integrationDBClean(); - await global.RedisClient?.flushall(); -}); - -const pkceChallenge = (code_verifier: string) => { - return createHash("sha256") - .update(code_verifier) - .digest("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=/g, ""); -}; - -const validParams = (app_id: string, pkce = false) => - ({ - // proof verification is mocked - ...semaphoreProofParamsMock, - app_id: app_id, - scope: "openid email", - response_type: "code", - redirect_uri: "http://localhost:3000/login", - state: "my_state", - ...(pkce - ? { - code_challenge: pkceChallenge("my_code_challenge"), - code_challenge_method: "S256", - } - : {}), - }) as Record; - -// TODO: Add additional test cases -describe("/api/v1/oidc/authorize", () => { - test("can get an auth code", async () => { - const dbQuery = await integrationDBExecuteQuery( - "SELECT * FROM app JOIN app_metadata ON app.id = app_metadata.app_id WHERE app_metadata.name = 'Sign In App' LIMIT 1;", - ); - const app_id = dbQuery.rows[0].app_id; - const req = new NextRequest("http://localhost:3000/api/v1/oidc/authorize", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(validParams(app_id)), - }); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(200); - expect(data).toEqual({ - code: expect.stringMatching(/^[a-f0-9]{16,30}$/), - }); - }); - - test("`redirect_uri` is required", async () => { - const app_id = await testGetDefaultApp(); - - const params = validParams(app_id); - delete params.redirect_uri; - - const req = new NextRequest("http://localhost:3000/api/v1/oidc/authorize", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(400); - expect(data).toEqual({ - attribute: "redirect_uri", - code: "validation_error", - detail: "This attribute is required.", - }); - }); - - test("invalid `redirect_uri` is rejected", async () => { - const app_id = await testGetDefaultApp(); - - const req = new NextRequest("http://localhost:3000/api/v1/oidc/authorize", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - ...validParams(app_id), - redirect_uri: "https://example.com/invalid", - }), - }); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(400); - expect(data).toEqual({ - attribute: "redirect_uri", - code: OIDCErrorCodes.InvalidRedirectURI, - detail: "Invalid redirect URI.", - app_id, - }); - }); - - test("can get an auth code with PKCE", async () => { - const dbQuery = await integrationDBExecuteQuery( - "SELECT * FROM app JOIN app_metadata ON app.id = app_metadata.app_id WHERE app_metadata.name = 'Sign In App' LIMIT 1;", - ); - const app_id = dbQuery.rows[0].app_id; - const req = new NextRequest("http://localhost:3000/api/v1/oidc/authorize", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(validParams(app_id, true)), - }); - - const response = await POST(req); - const data = await response.json(); - - expect(response.status).toBe(200); - expect(data).toEqual({ - code: expect.stringMatching(/^[a-f0-9]{16,30}$/), - }); - - const code = data.code; - - const { rows } = await integrationDBExecuteQuery( - `SELECT * FROM public.auth_code WHERE auth_code = '${code}' LIMIT 1;`, - ); - const { code_challenge, code_challenge_method } = rows[0]; - - expect(code_challenge_method).toEqual("S256"); - expect(code_challenge).toEqual(pkceChallenge("my_code_challenge")); - }); -}); diff --git a/web/tests/integration/oidc/token.test.ts b/web/tests/integration/oidc/token.test.ts deleted file mode 100644 index b553a07f9..000000000 --- a/web/tests/integration/oidc/token.test.ts +++ /dev/null @@ -1,749 +0,0 @@ -import { POST } from "@/api/v1/oidc/token"; -import { createHash } from "crypto"; -import * as jose from "jose"; -import { NextRequest, NextResponse } from "next/server"; -import { publicJwk } from "tests/api/__mocks__/jwk"; -import { integrationDBClean, integrationDBExecuteQuery } from "../setup"; -import { setClientSecret, testGetSignInApp } from "../test-utils"; - -jest.mock("@/api/helpers/kms", () => - require("tests/api/__mocks__/kms.mock.ts"), -); - -const pkceChallenge = (code_verifier: string) => { - return createHash("sha256") - .update(code_verifier) - .digest("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=/g, ""); -}; - -beforeEach(async () => await integrationDBClean()); - -describe("/api/v1/oidc/token", () => { - test("can exchange one-time auth code", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(200); - - const data = await response.json(); - const { access_token, id_token, token_type, expires_in, scope } = data; - expect(access_token).toBeTruthy(); - expect(id_token).toEqual(access_token); - expect(token_type).toEqual("Bearer"); - expect(expires_in).toEqual(3600); - expect(scope).toEqual("openid email"); - - // Verify that the auth code is deleted - const result = await integrationDBExecuteQuery( - "SELECT id FROM auth_code WHERE app_id = $1 AND auth_code = $2", - [app_id, "83a313c5939399ba017d2381"], - ); - expect(result.rowCount).toEqual(0); - - // Make sure the proper error response is now sent - const request2 = new NextRequest( - "http://localhost:3000/api/v1/oidc/token", - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - redirect_uri: "http://localhost:3000/login", - }), - }, - ); - - const response2 = (await POST(request2)) as NextResponse; - expect(response2.status).toBe(400); - const errorData = await response2.json(); - expect(errorData).toEqual( - expect.objectContaining({ - detail: "Invalid authorization code.", - code: "invalid_grant", - }), - ); - }); - - test("access_token is valid", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, verification_level, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6, $7)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - "orb", - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(200); - - const { access_token } = await response.json(); - - const { payload } = await jose.jwtVerify( - access_token, - await jose.importJWK(publicJwk, "RS256"), - { - issuer: process.env.JWT_ISSUER, - }, - ); - - expect(payload).toEqual( - expect.objectContaining({ - sub: "0x000000000000000111111111111", - aud: app_id, - iss: process.env.JWT_ISSUER, - exp: expect.any(Number), - iat: expect.any(Number), - jti: expect.any(String), - scope: "openid email", - email: "0x000000000000000111111111111@id.worldcoin.org", - "https://id.worldcoin.org/beta": expect.objectContaining({ - likely_human: "strong", - credential_type: "orb", - }), - "https://id.worldcoin.org/v1": { - verification_level: "orb", - }, - }), - ); - }); - - test("form-urlencoded with UTF-8 charset is accepted", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, verification_level, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6, $7)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - "orb", - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(200); - - const { access_token } = await response.json(); - - const { payload } = await jose.jwtVerify( - access_token, - await jose.importJWK(publicJwk, "RS256"), - { - issuer: process.env.JWT_ISSUER, - }, - ); - - expect(payload).toEqual( - expect.objectContaining({ - sub: "0x000000000000000111111111111", - aud: app_id, - iss: process.env.JWT_ISSUER, - exp: expect.any(Number), - iat: expect.any(Number), - jti: expect.any(String), - scope: "openid email", - email: "0x000000000000000111111111111@id.worldcoin.org", - "https://id.worldcoin.org/beta": expect.objectContaining({ - likely_human: "strong", - credential_type: "orb", - }), - "https://id.worldcoin.org/v1": { - verification_level: "orb", - }, - }), - ); - }); - - test("successfully validates PKCE", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code with PKCE - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, code_challenge, code_challenge_method, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - pkceChallenge("my_code_challenge"), - "S256", - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - code_verifier: "my_code_challenge", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(200); - - const { access_token, id_token, token_type, expires_in, scope } = - await response.json(); - expect(access_token).toBeTruthy(); - expect(id_token).toEqual(access_token); - expect(token_type).toEqual("Bearer"); - expect(expires_in).toEqual(3600); - expect(scope).toEqual("openid email"); - - // Verify that the auth code is deleted - const result = await integrationDBExecuteQuery( - "SELECT id FROM auth_code WHERE app_id = $1 AND auth_code = $2", - [app_id, "83a313c5939399ba017d2381"], - ); - expect(result.rowCount).toEqual(0); - }); - - test("rejects invalid PKCE", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code with PKCE - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, code_challenge, code_challenge_method, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - pkceChallenge("my_code_challenge"), - "S256", - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - code_verifier: "invalid_code_challenge", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(400); - const errorData = await response.json(); - expect(errorData).toEqual({ - attribute: "code_verifier", - code: "invalid_request", - detail: "Invalid code verifier.", - error: "invalid_request", - error_description: "Invalid code verifier.", - }); - - // Verify that the auth code is deleted - const result = await integrationDBExecuteQuery( - "SELECT id FROM auth_code WHERE app_id = $1 AND auth_code = $2", - [app_id, "83a313c5939399ba017d2381"], - ); - expect(result.rowCount).toEqual(0); - }); - - test("prevent PKCE downgrade", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code with PKCE - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, code_challenge, code_challenge_method, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - pkceChallenge("my_code_challenge"), - "S256", - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(400); - const errorData = await response.json(); - expect(errorData).toEqual({ - attribute: "code_verifier", - code: "invalid_request", - detail: "Missing code verifier.", - error: "invalid_request", - error_description: "Missing code verifier.", - }); - - // Verify that the auth code is deleted - const result = await integrationDBExecuteQuery( - "SELECT id FROM auth_code WHERE app_id = $1 AND auth_code = $2", - [app_id, "83a313c5939399ba017d2381"], - ); - expect(result.rowCount).toEqual(0); - }); - - test("error when PKCE not expected", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code with PKCE - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - code_verifier: "my_code_challenge", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(400); - const errorData = await response.json(); - expect(errorData).toEqual({ - code: "invalid_request", - error: "invalid_request", - attribute: "code_verifier", - detail: "Code verifier was not expected.", - error_description: "Code verifier was not expected.", - }); - - // Verify that the auth code is deleted - const result = await integrationDBExecuteQuery( - "SELECT id FROM auth_code WHERE app_id = $1 AND auth_code = $2", - [app_id, "83a313c5939399ba017d2381"], - ); - expect(result.rowCount).toEqual(0); - }); - - test("properly sets CORS headers", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code with PKCE - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, code_challenge, code_challenge_method, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - pkceChallenge("my_code_challenge"), - "S256", - "http://localhost:3000/login", - ], - ); - - const notPKCERequest = new NextRequest( - "http://localhost:3000/api/v1/oidc/token", - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - client_secret, - client_id: app_id, - code: "83a313c5939399ba017d2381", - grant_type: "authorization_code", - redirect_uri: "http://localhost:3000/login", - }), - }, - ); - - const notPKCEResponse = (await POST(notPKCERequest)) as NextResponse; - expect( - notPKCEResponse.headers.get("access-control-allow-origin"), - ).toBeNull(); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - code_verifier: "my_code_challenge", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.headers.get("access-control-allow-origin")).toEqual("*"); - }); - - test("successfully validates single redirect_uri", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(200); - - const { access_token, id_token, token_type, expires_in, scope } = - await response.json(); - expect(access_token).toBeTruthy(); - expect(id_token).toEqual(access_token); - expect(token_type).toEqual("Bearer"); - expect(expires_in).toEqual(3600); - expect(scope).toEqual("openid email"); - - // Verify that the auth code is deleted - const result = await integrationDBExecuteQuery( - "SELECT id FROM auth_code WHERE app_id = $1 AND auth_code = $2", - [app_id, "83a313c5939399ba017d2381"], - ); - expect(result.rowCount).toEqual(0); - }); - - test("allows no redirect_uri when only one set", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // Insert a valid auth code - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(200); - - const { access_token, id_token, token_type, expires_in, scope } = - await response.json(); - expect(access_token).toBeTruthy(); - expect(id_token).toEqual(access_token); - expect(token_type).toEqual("Bearer"); - expect(expires_in).toEqual(3600); - expect(scope).toEqual("openid email"); - - // Verify that the auth code is deleted - const result = await integrationDBExecuteQuery( - "SELECT id FROM auth_code WHERE app_id = $1 AND auth_code = $2", - [app_id, "83a313c5939399ba017d2381"], - ); - expect(result.rowCount).toEqual(0); - }); - - test("blocks no redirect_uri when multiple set", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // insert second redirect_uri - await integrationDBExecuteQuery( - "INSERT INTO redirect (action_id, redirect_uri) VALUES ((SELECT id FROM action WHERE app_id = $1 AND action = '') , $2)", - [app_id, "http://localhost:3000/login2"], - ); - - // Insert a valid auth code - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(400); - - const { code, detail, attribute } = await response.json(); - expect(code).toEqual("invalid_request"); - expect(detail).toEqual("Missing redirect URI."); - expect(attribute).toEqual("redirect_uri"); - }); - - test("blocks wrong redirect_uri when multiple set", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // insert second redirect_uri - await integrationDBExecuteQuery( - "INSERT INTO redirect (action_id, redirect_uri) VALUES ((SELECT id FROM action WHERE app_id = $1 AND action = '') , $2)", - [app_id, "http://localhost:3000/login2"], - ); - - // Insert a valid auth code - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - redirect_uri: "http://localhost:3000/login2", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(400); - - const { code, detail, attribute } = await response.json(); - expect(code).toEqual("invalid_request"); - expect(detail).toEqual("Invalid redirect URI."); - expect(attribute).toEqual("redirect_uri"); - }); - - test("accepts correct redirect_uri when multiple set", async () => { - const app_id = await testGetSignInApp(); - const { client_secret } = await setClientSecret(app_id); - - // insert second redirect_uri - await integrationDBExecuteQuery( - "INSERT INTO redirect (action_id, redirect_uri) VALUES ((SELECT id FROM action WHERE app_id = $1 AND action = '') , $2)", - [app_id, "http://localhost:3000/login2"], - ); - - // Insert a valid auth code - await integrationDBExecuteQuery( - "INSERT INTO auth_code (app_id, auth_code, expires_at, nullifier_hash, scope, redirect_uri) VALUES ($1, $2, $3, $4, $5, $6)", - [ - app_id, - "83a313c5939399ba017d2381", - "2030-09-01T00:00:00.000Z", - "0x000000000000000111111111111", - '["openid", "email"]', - "http://localhost:3000/login", - ], - ); - - const request = new NextRequest("http://localhost:3000/api/v1/oidc/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: "83a313c5939399ba017d2381", - client_id: app_id, - client_secret, - grant_type: "authorization_code", - redirect_uri: "http://localhost:3000/login", - }), - }); - - const response = (await POST(request)) as NextResponse; - expect(response.status).toBe(200); - - const { access_token, id_token, token_type, expires_in, scope } = - await response.json(); - expect(access_token).toBeTruthy(); - expect(id_token).toEqual(access_token); - expect(token_type).toEqual("Bearer"); - expect(expires_in).toEqual(3600); - expect(scope).toEqual("openid email"); - - // Verify that the auth code is deleted - const result = await integrationDBExecuteQuery( - "SELECT id FROM auth_code WHERE app_id = $1 AND auth_code = $2", - [app_id, "83a313c5939399ba017d2381"], - ); - expect(result.rowCount).toEqual(0); - }); -}); diff --git a/web/tests/integration/team.test.ts b/web/tests/integration/team.test.ts index 1b00bb3e8..08a63255c 100644 --- a/web/tests/integration/team.test.ts +++ b/web/tests/integration/team.test.ts @@ -183,7 +183,7 @@ describe("user role", () => { const memberRoleTeamId = memberRoleMemberships[0].team_id; const req = new NextRequest( - `${process.env.NEXT_PUBLIC_APP_URL}/api/hasura/reset-client-secret`, + `${process.env.NEXT_PUBLIC_APP_URL}/api/hasura/invite-team-members`, { method: "POST", headers: { @@ -219,7 +219,7 @@ describe("user role", () => { const tokenTeamId = teams[1].id; const req = new NextRequest( - `${process.env.NEXT_PUBLIC_APP_URL}/api/hasura/reset-client-secret`, + `${process.env.NEXT_PUBLIC_APP_URL}/api/hasura/invite-team-members`, { method: "POST", headers: { @@ -255,7 +255,7 @@ describe("user role", () => { const tokenTeamId = teamMemberships[0].team_id; const req = new NextRequest( - `${process.env.NEXT_PUBLIC_APP_URL}/api/hasura/reset-client-secret`, + `${process.env.NEXT_PUBLIC_APP_URL}/api/hasura/invite-team-members`, { method: "POST", headers: { diff --git a/web/tests/unit/check-flow-type.test.ts b/web/tests/unit/check-flow-type.test.ts deleted file mode 100644 index b58fbb226..000000000 --- a/web/tests/unit/check-flow-type.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { checkFlowType } from "@/api/helpers/oidc"; -import { OIDCFlowType } from "@/lib/types"; - -describe("Check flow type", () => { - test("Detects authorization code flow", () => { - const validResponseTypes = ["code"]; - - validResponseTypes.forEach((responseType) => { - const responseTypes = responseType.split(" "); - expect(checkFlowType(responseTypes)).toBe(OIDCFlowType.AuthorizationCode); - }); - }); - - test("Detects implicit flow", () => { - const validResponseTypes = ["token id_token", "id_token token", "id_token"]; - validResponseTypes.forEach((responseType) => { - const responseTypes = responseType.split(" "); - expect(checkFlowType(responseTypes)).toBe(OIDCFlowType.Implicit); - }); - }); - - test("Detects hybrid flow", () => { - const validResponseTypes = [ - "code id_token", - "id_token code", - "code token", - "token code", - "code id_token token", - "code token id_token", - "token code id_token", - "token id_token code", - "id_token code token", - "id_token token code", - ]; - - validResponseTypes.forEach((responseType) => { - const responseTypes = responseType.split(" "); - expect(checkFlowType(responseTypes)).toBe(OIDCFlowType.Hybrid); - }); - }); - - test("Detects `token` flow", () => { - const validResponseTypes = ["token"]; - - validResponseTypes.forEach((responseType) => { - const responseTypes = responseType.split(" "); - expect(checkFlowType(responseTypes)).toBe(OIDCFlowType.Token); - }); - }); - - test("Detects invalid flow", () => { - const invalidResponseTypes = ["value", "value1 value2"]; - - invalidResponseTypes.forEach((responseType) => { - const responseTypes = responseType.split(" "); - expect(checkFlowType(responseTypes)).toBe(null); - }); - }); -}); From f1599a91caa85978b9b5eb11128baaa8c3692225 Mon Sep 17 00:00:00 2001 From: 0x1 <13666360+0x1@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:38:27 -0800 Subject: [PATCH 3/4] remove unused OIDC types and dead KMS code --- web/api/helpers/kms.ts | 46 ------------------------------------------ web/lib/types.ts | 14 ------------- 2 files changed, 60 deletions(-) diff --git a/web/api/helpers/kms.ts b/web/api/helpers/kms.ts index 7779c6c41..d2fe83125 100644 --- a/web/api/helpers/kms.ts +++ b/web/api/helpers/kms.ts @@ -6,63 +6,17 @@ import "server-only"; import { logger } from "@/lib/logger"; import { - CreateKeyCommand, DescribeKeyCommand, - GetPublicKeyCommand, KMSClient, - KeySpec, ScheduleKeyDeletionCommand, } from "@aws-sdk/client-kms"; -export type CreateKeyResult = - | { - keyId: string; - publicKey: string; - createdAt: Date; - } - | undefined; - export const getKMSClient = async (region?: string) => { return new KMSClient({ region: region ?? process.env.AWS_REGION_NAME, }); }; -export const createKMSKey = async ( - client: KMSClient, - alg: KeySpec, -): Promise => { - try { - const { KeyMetadata } = await client.send( - new CreateKeyCommand({ - KeySpec: alg, - KeyUsage: "SIGN_VERIFY", - Description: `Developer Portal JWK for Sign in with World ID. Created: ${new Date().toISOString()}`, - Tags: [{ TagKey: "app", TagValue: "developer-portal" }], - }), - ); - - const keyId = KeyMetadata?.KeyId; - const createdAt = KeyMetadata?.CreationDate; - - if (keyId && createdAt) { - const { PublicKey } = await client.send( - new GetPublicKeyCommand({ KeyId: keyId }), - ); - - if (PublicKey) { - const publicKey = `-----BEGIN PUBLIC KEY----- -${Buffer.from(PublicKey).toString("base64")} ------END PUBLIC KEY-----`; - - return { keyId, publicKey, createdAt }; - } - } - } catch (error) { - logger.error("Error creating key.", { error }); - } -}; - export const getKMSKeyStatus = async (client: KMSClient, keyId: string) => { try { const { KeyMetadata } = await client.send( diff --git a/web/lib/types.ts b/web/lib/types.ts index 799b36709..83736928f 100644 --- a/web/lib/types.ts +++ b/web/lib/types.ts @@ -1,10 +1,3 @@ -export enum OIDCFlowType { - AuthorizationCode = "authorization_code", - Implicit = "implicit", - Hybrid = "hybrid", - Token = "token", -} - import { InsertMembershipMutation } from "@/api/create-team/graphql/insert-membership.generated"; /** * This file contains the main types for both the frontend and backend. @@ -63,13 +56,6 @@ export type ActionStatsModel = Array<{ total_cumulative: number; }>; -export enum OIDCResponseType { - Code = "code", // authorization code - JWT = "jwt", // implicit flow - IdToken = "id_token", - Token = "token", -} - export interface IInternalError { message: string; code: string; From 21b6280dd4d939c385735b87c55f1db736138070 Mon Sep 17 00:00:00 2001 From: 0x1 <13666360+0x1@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:11:33 -0800 Subject: [PATCH 4/4] cleanup mocks --- web/tests/api/__mocks__/kms.mock.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/web/tests/api/__mocks__/kms.mock.ts b/web/tests/api/__mocks__/kms.mock.ts index e4bdb414d..072f83c3d 100644 --- a/web/tests/api/__mocks__/kms.mock.ts +++ b/web/tests/api/__mocks__/kms.mock.ts @@ -1,5 +1,5 @@ -import { createPrivateKey, createPublicKey, createSign } from "crypto"; -import { privateJwk, publicJwk } from "./jwk"; +import { createPrivateKey, createSign } from "crypto"; +import { privateJwk } from "./jwk"; module.exports = { getKMSClient: jest.fn().mockImplementation(() => ({ @@ -16,13 +16,5 @@ module.exports = { }; }), })), - createKMSKey: jest.fn().mockImplementation(async () => { - const key = createPublicKey({ format: "jwk", key: publicJwk }); - const pemKey = key.export({ type: "pkcs1", format: "pem" }); - return { - keyId: "test-kms-key-id", - publicKey: pemKey, - }; - }), scheduleKeyDeletion: jest.fn(), };