diff --git a/.changeset/stale-knives-help.md b/.changeset/stale-knives-help.md new file mode 100644 index 000000000..c40eb6b06 --- /dev/null +++ b/.changeset/stale-knives-help.md @@ -0,0 +1,69 @@ +--- +'hive': major +--- + +**BREAKING** Remove support for `supertokens` service and replace it with native authentication solution. + +## Upgrade Guide + +Adjust your docker compose file like the following: +- Remove `services.supertokens` from your `docker-compose.community.yml` file +- Remove the following environment variables from the `services.server.environment` + - `SUPERTOKENS_CONNECTION_URI=` + - `SUPERTOKENS_API_KEY=` +- Set the following environment variables for `services.server.environment` + - `SUPERTOKENS_REFRESH_TOKEN_KEY=` + - `SUPERTOKENS_ACCESS_TOKEN_KEY=` + +### Set the refresh token key + +#### Extract from existing `supertokens` deployment + +This method works if you use supertokens before and want to have existing user sessions to continue working. +If you want to avoid messing with the database, you can also create a new refresh token key from scratch, the drawback is that users are forced to login again. + +Extract the refresh token key from the supertokens database +```sql +SELECT "value" FROM "supertokens_key_value" WHERE "name" = 'refresh_token_key'; +``` + +The key should look similar to this: `1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0` + +Update the docker compose `services.server.environment.SUPERTOKENS_REFRESH_TOKEN_KEY` environment variable value to this string. + +#### Create from scratch + +Run the following command to create a new refresh key from scratch: + +```sh +echo "1000:$(openssl rand -hex 64):$(openssl rand -hex 64)" +``` + +### Set the access token key + +Generate a new access token key using the following instructions: + +```sh +# 1. Generate a unique key name. 'uuidgen' is great for this. +# You can replace this with any string you like, e.g., KEY_NAME="my-app-key-1" +KEY_NAME=$(uuidgen) +# 2. Generate a 2048-bit RSA private key in PEM format, held in memory. +PRIVATE_KEY_PEM=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048) +# 3. Extract the corresponding public key from the private key, also held in memory. +PUBLIC_KEY_PEM=$(echo "$PRIVATE_KEY_PEM" | openssl rsa -pubout) +# 4. Strip the headers/footers and newlines from the private key PEM +# to get just the raw Base64 data. +PRIVATE_KEY_DATA=$(echo "$PRIVATE_KEY_PEM" | awk 'NF {if (NR!=1 && $0!~/-----END/) print}' | tr -d '\n') +# 5. Do the same for the public key PEM. +PUBLIC_KEY_DATA=$(echo "$PUBLIC_KEY_PEM" | awk 'NF {if (NR!=1 && $0!~/-----END/) print}' | tr -d '\n') +# 6. Echo the final formatted string to the console. +echo "${KEY_NAME}|${PUBLIC_KEY_DATA}|${PRIVATE_KEY_DATA}" +``` + +Update the docker compose `services.server.environment.SUPERTOKENS_ACCESS_TOKEN_KEY` environment variable value to the formatted string output. + +## Conclusion + +After performing this updates you can run Hive Console without the need for the `supertokens` service. All the relevant authentication logic resides within the `server` container instead. + +Existing users in the supertokens system will continue to exist when running without the `supertokens` service. diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index 7c47dae79..4dcad7a20 100644 --- a/cypress/e2e/app.cy.ts +++ b/cypress/e2e/app.cy.ts @@ -250,7 +250,9 @@ describe('oidc', () => { cy.clearAllLocalStorage(); cy.clearAllSessionStorage(); cy.visit('/auth/oidc?id=invalid'); - cy.get('[data-cy="auth-card-header-description"]').contains('Could not find OIDC integration.'); + cy.get('[data-cy="auth-card-header-description"]').contains( + 'Something went wrong. Please try again', + ); }); describe('requireInvitation', () => { @@ -278,7 +280,7 @@ describe('oidc', () => { // Check if OIDC authentication failed as intended cy.get(`a[href="/${slug}"]`).should('not.exist'); - cy.contains('not invited'); + cy.contains('Sign in not allowed.'); }); }); }); diff --git a/deployment/index.ts b/deployment/index.ts index e1b4a32ee..31a72ec10 100644 --- a/deployment/index.ts +++ b/deployment/index.ts @@ -24,7 +24,6 @@ import { deployS3, deployS3AuditLog, deployS3Mirror } from './services/s3'; import { deploySchema } from './services/schema'; import { configureSentry } from './services/sentry'; import { configureSlackApp } from './services/slack-app'; -import { deploySuperTokens } from './services/supertokens'; import { deployTokens } from './services/tokens'; import { deployUsage } from './services/usage'; import { deployUsageIngestor } from './services/usage-ingestor'; @@ -203,7 +202,6 @@ deployWorkflows({ redis, }); -const supertokens = deploySuperTokens(postgres, { dependencies: [dbMigrations] }, environment); const zendesk = configureZendesk({ environment }); const githubApp = configureGithubApp(); const slackApp = configureSlackApp(); @@ -222,7 +220,6 @@ const graphql = deployGraphQL({ usage, cdn, commerce, - supertokens, s3, s3Mirror, s3AuditLog, diff --git a/deployment/services/db-migrations.ts b/deployment/services/db-migrations.ts index e21907d12..e8534527d 100644 --- a/deployment/services/db-migrations.ts +++ b/deployment/services/db-migrations.ts @@ -47,7 +47,6 @@ export function deployDbMigrations({ // Since K8s job are immutable, we can't edit or ask K8s to re-run a Job, so we are doing a // pseudo change to an env var, which causes Pulumi to re-create the Job. IGNORE_RERUN_NONCE: force ? Date.now().toString() : '0', - SUPERTOKENS_AT_HOME: '1', }, }, [clickhouse.deployment, clickhouse.service, ...(dependencies || [])], diff --git a/deployment/services/environment.ts b/deployment/services/environment.ts index 690215075..e37c4aca1 100644 --- a/deployment/services/environment.ts +++ b/deployment/services/environment.ts @@ -48,9 +48,6 @@ export function prepareEnvironment(input: { general: { replicas: isProduction || isStaging ? 3 : 1, }, - supertokens: { - replicas: isProduction || isStaging ? 3 : 1, - }, envoy: { replicas: isProduction || isStaging ? 3 : 1, cpuLimit: isProduction ? '1500m' : '120m', diff --git a/deployment/services/graphql.ts b/deployment/services/graphql.ts index 41a077902..cb5730c15 100644 --- a/deployment/services/graphql.ts +++ b/deployment/services/graphql.ts @@ -16,7 +16,6 @@ import { Redis } from './redis'; import { S3 } from './s3'; import { Schema } from './schema'; import { Sentry } from './sentry'; -import { Supertokens } from './supertokens'; import { Tokens } from './tokens'; import { Usage } from './usage'; import { Zendesk } from './zendesk'; @@ -40,7 +39,6 @@ export function deployGraphQL({ usage, commerce, dbMigrations, - supertokens, s3, s3Mirror, s3AuditLog, @@ -68,7 +66,6 @@ export function deployGraphQL({ usage: Usage; dbMigrations: DbMigrations; commerce: CommerceService; - supertokens: Supertokens; zendesk: Zendesk; docker: Docker; sentry: Sentry; @@ -144,7 +141,6 @@ export function deployGraphQL({ ZENDESK_SUPPORT: zendesk.enabled ? '1' : '0', INTEGRATION_GITHUB: '1', // Auth - SUPERTOKENS_CONNECTION_URI: supertokens.localEndpoint, AUTH_GITHUB: '1', AUTH_GOOGLE: '1', AUTH_ORGANIZATION_OIDC: '1', @@ -155,7 +151,6 @@ export function deployGraphQL({ ? observability.tracingEndpoint : '', S3_MIRROR: '1', - SUPERTOKENS_AT_HOME: '1', }, exposesMetrics: true, port: 4000, @@ -209,7 +204,6 @@ export function deployGraphQL({ .withSecret('S3_AUDIT_LOG_BUCKET_NAME', s3AuditLog.secret, 'bucket') .withSecret('S3_AUDIT_LOG_ENDPOINT', s3AuditLog.secret, 'endpoint') // Auth - .withSecret('SUPERTOKENS_API_KEY', supertokens.secret, 'apiKey') .withSecret('AUTH_GITHUB_CLIENT_ID', githubOAuthSecret, 'clientId') .withSecret('AUTH_GITHUB_CLIENT_SECRET', githubOAuthSecret, 'clientSecret') .withSecret('AUTH_GOOGLE_CLIENT_ID', googleOAuthSecret, 'clientId') diff --git a/deployment/services/supertokens.ts b/deployment/services/supertokens.ts deleted file mode 100644 index eb7ccf17f..000000000 --- a/deployment/services/supertokens.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as kx from '@pulumi/kubernetesx'; -import * as pulumi from '@pulumi/pulumi'; -import * as random from '@pulumi/random'; -import { serviceLocalEndpoint } from '../utils/local-endpoint'; -import { ServiceSecret } from '../utils/secrets'; -import { createService } from '../utils/service-deployment'; -import { Environment } from './environment'; -import { Postgres } from './postgres'; - -export class SupertokensSecret extends ServiceSecret<{ - apiKey: string | pulumi.Output; -}> {} - -export function deploySuperTokens( - postgres: Postgres, - resourceOptions: { - dependencies: pulumi.Resource[]; - }, - environment: Environment, -) { - const supertokensApiKey = new random.RandomPassword('supertokens-api-key', { - length: 31, - special: false, - }).result; - - const secret = new SupertokensSecret('supertokens', { - apiKey: supertokensApiKey, - }); - - const port = 3567; - const pb = new kx.PodBuilder({ - restartPolicy: 'Always', - containers: [ - { - image: 'registry.supertokens.io/supertokens/supertokens-postgresql:9.3', - name: 'supertokens', - ports: { - http: port, - }, - startupProbe: { - initialDelaySeconds: 15, - periodSeconds: 20, - failureThreshold: 5, - timeoutSeconds: 5, - httpGet: { - path: '/hello', - port, - }, - }, - readinessProbe: { - initialDelaySeconds: 5, - periodSeconds: 20, - failureThreshold: 5, - timeoutSeconds: 5, - httpGet: { - path: '/hello', - port, - }, - }, - livenessProbe: { - initialDelaySeconds: 3, - periodSeconds: 20, - failureThreshold: 10, - timeoutSeconds: 5, - httpGet: { - path: '/hello', - port, - }, - }, - env: { - POSTGRESQL_TABLE_NAMES_PREFIX: 'supertokens', - POSTGRESQL_CONNECTION_URI: { - secretKeyRef: { - name: postgres.secret.record.metadata.name, - key: 'connectionStringPostgresql', - }, - }, - API_KEYS: { - secretKeyRef: { - name: secret.record.metadata.name, - key: 'apiKey', - }, - }, - }, - }, - ], - }); - - const deployment = new kx.Deployment( - 'supertokens', - { - spec: pb.asDeploymentSpec({ replicas: environment.podsConfig.supertokens.replicas }), - }, - { - dependsOn: resourceOptions.dependencies, - }, - ); - - const service = createService('supertokens', deployment); - - return { - deployment, - service, - localEndpoint: serviceLocalEndpoint(service), - secret, - }; -} - -export type Supertokens = ReturnType; diff --git a/docker/docker-compose.community.yml b/docker/docker-compose.community.yml index ed35cf42b..7472d4ce3 100644 --- a/docker/docker-compose.community.yml +++ b/docker/docker-compose.community.yml @@ -89,22 +89,6 @@ services: volumes: - './.hive/redis/db:/bitnami/redis/data' - supertokens: - image: registry.supertokens.io/supertokens/supertokens-postgresql:9.3 - depends_on: - db: - condition: service_healthy - networks: - - 'stack' - environment: - POSTGRESQL_USER: '${POSTGRES_USER}' - POSTGRESQL_PASSWORD: '${POSTGRES_PASSWORD}' - POSTGRESQL_DATABASE_NAME: '${POSTGRES_DB}' - POSTGRESQL_TABLE_NAMES_PREFIX: 'supertokens' - POSTGRESQL_HOST: db - POSTGRESQL_PORT: 5432 - API_KEYS: '${SUPERTOKENS_API_KEY}' - s3: image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z command: server /data --console-address ":9001" @@ -234,9 +218,9 @@ services: # Auth AUTH_ORGANIZATION_OIDC: '1' AUTH_REQUIRE_EMAIL_VERIFICATION: '0' - SUPERTOKENS_CONNECTION_URI: http://supertokens:3567 - SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}' GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082 + SUPERTOKENS_REFRESH_TOKEN_KEY: '${SUPERTOKENS_REFRESH_TOKEN_KEY}' + SUPERTOKENS_ACCESS_TOKEN_KEY: '${SUPERTOKENS_ACCESS_TOKEN_KEY}' # Tracing OPENTELEMETRY_COLLECTOR_ENDPOINT: '${OPENTELEMETRY_COLLECTOR_ENDPOINT:-}' SENTRY: '${SENTRY:-0}' diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 44f3852c1..d63332668 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -123,25 +123,6 @@ services: volumes: - ./.hive-dev/broker/db:/var/lib/kafka/data - supertokens: - image: registry.supertokens.io/supertokens/supertokens-postgresql:9.3 - mem_limit: 300m - depends_on: - db: - condition: service_healthy - networks: - - 'stack' - ports: - - '3567:3567' - environment: - POSTGRESQL_USER: postgres - POSTGRESQL_PASSWORD: postgres - POSTGRESQL_DATABASE_NAME: registry - POSTGRESQL_TABLE_NAMES_PREFIX: 'supertokens' - POSTGRESQL_HOST: db - POSTGRESQL_PORT: 5432 - API_KEYS: bubatzbieber6942096420 - oidc-server-mock: image: ghcr.io/soluto/oidc-server-mock:0.8.6 mem_limit: 200m diff --git a/docker/docker-compose.end2end.yml b/docker/docker-compose.end2end.yml index 0e12549f0..1227a185b 100644 --- a/docker/docker-compose.end2end.yml +++ b/docker/docker-compose.end2end.yml @@ -33,10 +33,6 @@ services: networks: - 'stack' - supertokens: - ports: - - '3567:3567' - db: ports: - '5432:5432' diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 64aca3495..57484e810 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -46,64 +46,6 @@ Add "user" field to ./docker/docker-compose.dev.yml - Open the UI (`http://localhost:3000` by default) and Sign in with any of the identity provider - Once this is done, you should be able to log in and use the project -#### Possible Issues During Setup - -If you encounter an error such as: - -``` -error: relation "supertokens_*" does not exist -``` - -it usually means that the **Supertokens database tables** were not initialized correctly. - -##### Steps to fix it - -1. **Ensure no local PostgreSQL instance is running** - - - The local PostgreSQL service on your machine might conflict with the one running in Docker. - - Stop any locally running PostgreSQL service and make sure the database used by Hive is the one - from Docker Compose. - -2. **Handle possible race conditions between `db` and `supertokens` containers** - - - This issue may occur if `supertokens` starts before the `db` container is fully initialized. - - To fix: - 1. Stop all running containers: - ```bash - docker compose -f ./docker/docker-compose.dev.yml down - ``` - 2. Start only the database: - ```bash - docker compose -f ./docker/docker-compose.dev.yml up db - ``` - 3. Wait until the database is ready (you should see “database system is ready to accept - connections” in logs). - 4. Start the `supertokens` service: - ```bash - docker compose -f ./docker/docker-compose.dev.yml up supertokens - ``` - 5. Once `supertokens` successfully creates all the tables, start the rest of the containers: - ```bash - docker compose -f ./docker/docker-compose.dev.yml up -d - ``` - -3. **If only Supertokens tables were created** - - Run the setup command again to ensure all services are initialized properly: - ```bash - pnpm local:setup - ``` - - Then restart the Hive Console using the VSCode “Start Hive” button. - -After completing these steps, reload the Hive UI at [http://localhost:3000](http://localhost:3000), -and you should be able to log in successfully. - -- Once you generate the token against your organization/personal account in hive, the same can be - added locally to `hive.json` within `packages/libraries/cli` which can be used to interact via the - hive cli with the registry (Use `http://localhost:3001/graphql` as the `registry.endpoint` value - in `hive.json`) -- Now you can use Hive locally. All other steps in this document are optional and only necessary if - you work on specific features. - ## Development Seed We have a script to feed your local instance of Hive with initial seed data. This step is optional. diff --git a/docs/architecture.puml b/docs/architecture.puml index 2f50f1aef..41386aefb 100644 --- a/docs/architecture.puml +++ b/docs/architecture.puml @@ -5,7 +5,6 @@ actor "CLI User" as cliuser actor "Running Server" as gqlserver component zookeeper -component supertokens queue kafka @@ -28,7 +27,6 @@ storageSvc ---d-> Postgres kafka -l-> zookeeper -app --> supertokens app --> server app --> emails @@ -49,9 +47,6 @@ server -d-> tokens server -d-> webhooks server -d-> schema server -d-> emails -server -d-> supertokens - -supertokens -> Postgres uiuser --> app cliuser --> server @@ -60,4 +55,4 @@ emails -[hidden]-> webhooks usage ----> tokens -@enduml \ No newline at end of file +@enduml diff --git a/integration-tests/.env b/integration-tests/.env index 8b1a2755f..6c23bf2b0 100644 --- a/integration-tests/.env +++ b/integration-tests/.env @@ -24,6 +24,5 @@ CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE=1000 EXTERNAL_COMPOSITION_SECRET=secretsecret LIMIT_CACHE_UPDATE_INTERVAL_MS=2000 NODE_OPTIONS=--enable-source-maps -SUPERTOKENS_AT_HOME=0 SUPERTOKENS_REFRESH_TOKEN_KEY=1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0 SUPERTOKENS_ACCESS_TOKEN_KEY=s-aaa5da0d-8678-46ef-b56d-9cd19e1cdb5b|MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjdpx/LNIAPKDCNgszZIzz2tepThK9/3mCWTrOjAJY8KI8YgMo5Gf+IwYvJ2VcpKYa36UJ70bD283P2O/yE/IjtXI6S9cJyd5LrYs+QDENc8au63iDy2iAiFpR1kO7cWQPDKK3OrD+hc5ZEHA3LN82Kb4ZnEA6tAulPfULVDU+RJfSWZOCE+LnkSZ8obvJjiMeknhNSSJko6V3WVuL5ToYfRIiOnueoTywB+3O3Mtp6lBj1j2rpQfO/qvLdRYLpDmLaoaScAyymWfeBp0hpwxd5Jm4vexyHgit2sK0S+tFl0pmh37iVGsRqPJEPISpEwQBcHOhKRj0uW+t/feK6U0WQIDAQAB|MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN2nH8s0gA8oMI2CzNkjPPa16lOEr3/eYJZOs6MAljwojxiAyjkZ/4jBi8nZVykphrfpQnvRsPbzc/Y7/IT8iO1cjpL1wnJ3kutiz5AMQ1zxq7reIPLaICIWlHWQ7txZA8Morc6sP6FzlkQcDcs3zYpvhmcQDq0C6U99QtUNT5El9JZk4IT4ueRJnyhu8mOIx6SeE1JImSjpXdZW4vlOhh9EiI6e56hPLAH7c7cy2nqUGPWPaulB87+q8t1FgukOYtqhpJwDLKZZ94GnSGnDF3kmbi97HIeCK3awrRL60WXSmaHfuJUaxGo8kQ8hKkTBAFwc6EpGPS5b63994rpTRZAgMBAAECggEBAIl96/IFS4svg/Z0oah3RySKa2g1EeUhAXClkqIJoXBCRD3nomiAY8+i6u8Wxp4QnQ/D1pJV5v6ky6XzZxYezsQzTtNGBkolJn4yMZEAPy3wmXbD6VLQ5jCudb6kAaZRUaYnTxUlr+Kd1BDq8qZ4ik/sNuQEL+Fo+12EgPGTYXov7lWwWjNbzuzMQMm7b1BDU7D/8s/lGg4wimJffVSd4C++buN4Jxm1n1hWWREl7jkJC0sp2J50cpt9IhIIhi8DOnGAcJ4aTtABEJdZyXlO0QllN/D5FEbZBC0Jkbl3lmaIo1WVEYdDpcbSLZxGeYD0CkH4CF/BzUpeHq7FU0HkqOkCgYEA5tgLRFC4CS/FtR3JQ1YifHN4J2bI3BEyje6eiI/n9jmm5zUN/WjCQ6ui2fPzbKAC3yD60lcCgrP7YAn4KFmOv9RS7ApamUH+AX5UBfHlhvwzi4U9eenu7UHH8XrxEHlAwUC9mQbaqzoR/A7jEg8qqincMDUCkk1kjP4nNgQSBFcCgYEAnU/KSQf79m57nXMv+ge9QWkAggnHO7baRox5qlMM6l1gTZB5yELaLNXeik9D2mhVwcpezcQo2Uf+B4MviVeqpoTzYwgdxYg+ebYPUzhd3Ya8ANRDwKCB7SSoRULEDpWebV6ngOc+ruv9ii3ZbVEi7ribtHo6w7rVVJc2bMEKns8CgYB9ssp/yoxLxE2dz7hWCEMDDUUx/1AENQEYNATzS5j9hGsTntodULvnaUBl+eZlEcQ+h5DMlEBzt1l79DHClvGaFx2IFiM7LKoJWiaajhtzo0TWBhlxlyZY3ubm4RD+7WeLU5tqBkdv0VEVtW2D2epbeivBvDvIOog0Ffh3+0NsRQKBgGPA8w84hugPy0dega/VNIfD49SSCsqs+uD9tzDwlSIQsD6/PNpmuh7wR7wA45Ad1TOb9l4Y46ZU5psw7vXyp34MlKHZxbc63BMmBbXJ6ovNIm6MK6J8pacRNbslyVlOOzYzbZhqCu+1KgNza4rMhpBGdEYPtC/ly91mPdbc2rU1AoGBALl6eZqBMahE0S19X5SO/xykGe4ALf74UWCsfKrVEO4Zd3IcELnir0uEPABWvd5C/EAaGoi/a2xgdwuKG32GMpinjoXJywzsquQC6N8CcFIzDiQXaL4j4lFztjgowqNs/YwpOGbm1Dyr3Av072jDPajQGP/xX4fFxBZFnyk1vnXT diff --git a/integration-tests/docker-compose.integration.yaml b/integration-tests/docker-compose.integration.yaml index b8cec1a9f..bbe3ca8fc 100644 --- a/integration-tests/docker-compose.integration.yaml +++ b/integration-tests/docker-compose.integration.yaml @@ -165,9 +165,10 @@ services: WEB_APP_URL: '${HIVE_APP_BASE_URL}' AUTH_ORGANIZATION_OIDC: '1' AUTH_REQUIRE_EMAIL_VERIFICATION: '1' - SUPERTOKENS_CONNECTION_URI: http://supertokens:3567 - SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}' GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082 + SUPERTOKENS_RATE_LIMIT: '0' + SUPERTOKENS_REFRESH_TOKEN_KEY: '1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0' + SUPERTOKENS_ACCESS_TOKEN_KEY: 's-aaa5da0d-8678-46ef-b56d-9cd19e1cdb5b|MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjdpx/LNIAPKDCNgszZIzz2tepThK9/3mCWTrOjAJY8KI8YgMo5Gf+IwYvJ2VcpKYa36UJ70bD283P2O/yE/IjtXI6S9cJyd5LrYs+QDENc8au63iDy2iAiFpR1kO7cWQPDKK3OrD+hc5ZEHA3LN82Kb4ZnEA6tAulPfULVDU+RJfSWZOCE+LnkSZ8obvJjiMeknhNSSJko6V3WVuL5ToYfRIiOnueoTywB+3O3Mtp6lBj1j2rpQfO/qvLdRYLpDmLaoaScAyymWfeBp0hpwxd5Jm4vexyHgit2sK0S+tFl0pmh37iVGsRqPJEPISpEwQBcHOhKRj0uW+t/feK6U0WQIDAQAB|MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN2nH8s0gA8oMI2CzNkjPPa16lOEr3/eYJZOs6MAljwojxiAyjkZ/4jBi8nZVykphrfpQnvRsPbzc/Y7/IT8iO1cjpL1wnJ3kutiz5AMQ1zxq7reIPLaICIWlHWQ7txZA8Morc6sP6FzlkQcDcs3zYpvhmcQDq0C6U99QtUNT5El9JZk4IT4ueRJnyhu8mOIx6SeE1JImSjpXdZW4vlOhh9EiI6e56hPLAH7c7cy2nqUGPWPaulB87+q8t1FgukOYtqhpJwDLKZZ94GnSGnDF3kmbi97HIeCK3awrRL60WXSmaHfuJUaxGo8kQ8hKkTBAFwc6EpGPS5b63994rpTRZAgMBAAECggEBAIl96/IFS4svg/Z0oah3RySKa2g1EeUhAXClkqIJoXBCRD3nomiAY8+i6u8Wxp4QnQ/D1pJV5v6ky6XzZxYezsQzTtNGBkolJn4yMZEAPy3wmXbD6VLQ5jCudb6kAaZRUaYnTxUlr+Kd1BDq8qZ4ik/sNuQEL+Fo+12EgPGTYXov7lWwWjNbzuzMQMm7b1BDU7D/8s/lGg4wimJffVSd4C++buN4Jxm1n1hWWREl7jkJC0sp2J50cpt9IhIIhi8DOnGAcJ4aTtABEJdZyXlO0QllN/D5FEbZBC0Jkbl3lmaIo1WVEYdDpcbSLZxGeYD0CkH4CF/BzUpeHq7FU0HkqOkCgYEA5tgLRFC4CS/FtR3JQ1YifHN4J2bI3BEyje6eiI/n9jmm5zUN/WjCQ6ui2fPzbKAC3yD60lcCgrP7YAn4KFmOv9RS7ApamUH+AX5UBfHlhvwzi4U9eenu7UHH8XrxEHlAwUC9mQbaqzoR/A7jEg8qqincMDUCkk1kjP4nNgQSBFcCgYEAnU/KSQf79m57nXMv+ge9QWkAggnHO7baRox5qlMM6l1gTZB5yELaLNXeik9D2mhVwcpezcQo2Uf+B4MviVeqpoTzYwgdxYg+ebYPUzhd3Ya8ANRDwKCB7SSoRULEDpWebV6ngOc+ruv9ii3ZbVEi7ribtHo6w7rVVJc2bMEKns8CgYB9ssp/yoxLxE2dz7hWCEMDDUUx/1AENQEYNATzS5j9hGsTntodULvnaUBl+eZlEcQ+h5DMlEBzt1l79DHClvGaFx2IFiM7LKoJWiaajhtzo0TWBhlxlyZY3ubm4RD+7WeLU5tqBkdv0VEVtW2D2epbeivBvDvIOog0Ffh3+0NsRQKBgGPA8w84hugPy0dega/VNIfD49SSCsqs+uD9tzDwlSIQsD6/PNpmuh7wR7wA45Ad1TOb9l4Y46ZU5psw7vXyp34MlKHZxbc63BMmBbXJ6ovNIm6MK6J8pacRNbslyVlOOzYzbZhqCu+1KgNza4rMhpBGdEYPtC/ly91mPdbc2rU1AoGBALl6eZqBMahE0S19X5SO/xykGe4ALf74UWCsfKrVEO4Zd3IcELnir0uEPABWvd5C/EAaGoi/a2xgdwuKG32GMpinjoXJywzsquQC6N8CcFIzDiQXaL4j4lFztjgowqNs/YwpOGbm1Dyr3Av072jDPajQGP/xX4fFxBZFnyk1vnXT' extra_hosts: - 'host.docker.internal:host-gateway' @@ -212,10 +213,6 @@ services: - ./.hive/clickhouse/db:/var/lib/clickhouse - ./configs/clickhouse:/etc/clickhouse-server/conf.d - supertokens: - ports: - - '3567:3567' - usage: environment: COMMERCE_ENDPOINT: '${COMMERCE_ENDPOINT}' diff --git a/integration-tests/local-dev.ts b/integration-tests/local-dev.ts index 42e206ddf..c4f11ca52 100644 --- a/integration-tests/local-dev.ts +++ b/integration-tests/local-dev.ts @@ -12,8 +12,6 @@ const __dirname = import.meta.dirname; const serverEnvVars = parse(readFileSync(__dirname + '/../packages/services/server/.env', 'utf-8')); applyEnv({ - SUPERTOKENS_CONNECTION_URI: serverEnvVars.SUPERTOKENS_CONNECTION_URI, - SUPERTOKENS_API_KEY: serverEnvVars.SUPERTOKENS_API_KEY, POSTGRES_USER: serverEnvVars.POSTGRES_USER, POSTGRES_PASSWORD: serverEnvVars.POSTGRES_PASSWORD, POSTGRES_DB: serverEnvVars.POSTGRES_DB, @@ -24,7 +22,6 @@ applyEnv({ CLICKHOUSE_USER: serverEnvVars.CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD: serverEnvVars.CLICKHOUSE_PASSWORD, HIVE_ENCRYPTION_SECRET: serverEnvVars.HIVE_ENCRYPTION_SECRET, - SUPERTOKENS_AT_HOME: serverEnvVars.SUPERTOKENS_AT_HOME, SUPERTOKENS_REFRESH_TOKEN_KEY: serverEnvVars.SUPERTOKENS_REFRESH_TOKEN_KEY, SUPERTOKENS_ACCESS_TOKEN_KEY: serverEnvVars.SUPERTOKENS_ACCESS_TOKEN_KEY, }); diff --git a/integration-tests/testkit/auth.ts b/integration-tests/testkit/auth.ts index b8368ee4c..cb81fdd28 100644 --- a/integration-tests/testkit/auth.ts +++ b/integration-tests/testkit/auth.ts @@ -1,5 +1,4 @@ import { DatabasePool } from 'slonik'; -import { z } from 'zod'; import { AccessTokenKeyContainer, hashPassword, @@ -9,62 +8,8 @@ import { NoopLogger } from '@hive/api/modules/shared/providers/logger'; import type { InternalApi } from '@hive/server'; import { createNewSession } from '@hive/server/supertokens-at-home/shared'; import { createTRPCProxyClient, httpLink } from '@trpc/client'; -import { ensureEnv } from './env'; import { getServiceHost } from './utils'; -const SignUpSignInUserResponseModel = z - .object({ - status: z.literal('OK'), - user: z.object({ - emails: z.array(z.string()), - id: z.string(), - timeJoined: z.number(), - }), - }) - .refine(response => response.user.emails.length === 1) - .transform(response => ({ - ...response, - user: { - id: response.user.id, - email: response.user.emails[0], - timeJoined: response.user.timeJoined, - }, - })); - -const signUpUserViaEmail = async ( - email: string, - password: string, -): Promise> => { - try { - const response = await fetch( - `${ensureEnv('SUPERTOKENS_CONNECTION_URI')}/appid-public/public/recipe/signup`, - { - method: 'POST', - headers: { - 'content-type': 'application/json; charset=UTF-8', - 'api-key': ensureEnv('SUPERTOKENS_API_KEY'), - 'cdi-version': '4.0', - }, - body: JSON.stringify({ - email, - password, - }), - }, - ); - const body = await response.text(); - - if (response.status !== 200) { - throw new Error(`Signup failed. ${response.status}.\n ${body}`); - } - - return SignUpSignInUserResponseModel.parse(JSON.parse(body)); - } catch (e) { - console.warn(`Failed to sign up:`, e); - - throw e; - } -}; - const createSessionAtHome = async ( supertokensStore: SuperTokensStore, superTokensUserId: string, @@ -116,103 +61,6 @@ const createSessionAtHome = async ( }; }; -const createSessionPayload = (payload: { - superTokensUserId: string; - userId: string; - oidcIntegrationId: string | null; - email: string; -}) => ({ - version: '2', - superTokensUserId: payload.superTokensUserId, - userId: payload.userId, - oidcIntegrationId: payload.oidcIntegrationId, - email: payload.email, -}); - -const CreateSessionModel = z.object({ - accessToken: z.object({ - token: z.string(), - }), - refreshToken: z.object({ - token: z.string(), - }), -}); - -const createSession = async ( - superTokensUserId: string, - email: string, - oidcIntegrationId: string | null, -) => { - try { - const graphqlAddress = await getServiceHost('server', 8082); - - const internalApi = createTRPCProxyClient({ - links: [ - httpLink({ - url: `http://${graphqlAddress}/trpc`, - fetch, - }), - ], - }); - - const ensureUserResult = await internalApi.ensureUser.mutate({ - superTokensUserId, - email, - oidcIntegrationId, - firstName: null, - lastName: null, - }); - if (!ensureUserResult.ok) { - throw new Error(ensureUserResult.reason); - } - - const sessionData = createSessionPayload({ - superTokensUserId, - userId: ensureUserResult.user.id, - oidcIntegrationId, - email, - }); - const payload = { - enableAntiCsrf: false, - userId: superTokensUserId, - userDataInDatabase: sessionData, - userDataInJWT: sessionData, - }; - - const response = await fetch( - `${ensureEnv('SUPERTOKENS_CONNECTION_URI')}/appid-public/public/recipe/session`, - { - method: 'POST', - headers: { - 'content-type': 'application/json; charset=UTF-8', - 'api-key': ensureEnv('SUPERTOKENS_API_KEY'), - rid: 'session', - 'cdi-version': '4.0', - }, - body: JSON.stringify(payload), - }, - ); - const body = await response.text(); - - if (response.status !== 200) { - throw new Error(`Create session failed. ${response.status}.\n ${body}`); - } - - const data = CreateSessionModel.parse(JSON.parse(body)); - - /** - * These are the required cookies that need to be set. - */ - return { - access_token: data.accessToken.token, - refresh_token: data.refreshToken.token, - }; - } catch (e) { - console.warn(`Failed to create session:`, e); - throw e; - } -}; - const password = 'ilikebigturtlesandicannotlie47'; const hashedPassword = await hashPassword(password); @@ -232,34 +80,14 @@ export async function authenticate( email: string, oidcIntegrationId?: string, ): Promise<{ access_token: string; refresh_token: string; supertokensUserId: string }> { - if (process.env.SUPERTOKENS_AT_HOME === '1') { - const supertokensStore = new SuperTokensStore(pool, new NoopLogger()); - if (!tokenResponsePromise[email]) { - tokenResponsePromise[email] = supertokensStore.createEmailPasswordUser({ - email, - passwordHash: hashedPassword, - }); - } - - const user = await tokenResponsePromise[email]!; - - return await createSessionAtHome( - supertokensStore, - user.userId, - email, - oidcIntegrationId ?? null, - ); - } - + const supertokensStore = new SuperTokensStore(pool, new NoopLogger()); if (!tokenResponsePromise[email]) { - tokenResponsePromise[email] = signUpUserViaEmail(email, password).then(res => ({ - email: res.user.email, - userId: res.user.id, - })); + tokenResponsePromise[email] = supertokensStore.createEmailPasswordUser({ + email, + passwordHash: hashedPassword, + }); } - return tokenResponsePromise[email]!.then(async data => ({ - ...(await createSession(data.userId, data.email, oidcIntegrationId ?? null)), - supertokensUserId: data.userId, - })); + const user = await tokenResponsePromise[email]!; + return await createSessionAtHome(supertokensStore, user.userId, email, oidcIntegrationId ?? null); } diff --git a/integration-tests/testkit/oidc-integration.ts b/integration-tests/testkit/oidc-integration.ts index 902a0a927..99db94859 100644 --- a/integration-tests/testkit/oidc-integration.ts +++ b/integration-tests/testkit/oidc-integration.ts @@ -314,7 +314,7 @@ export async function createOIDCIntegration(args: { .parse(rawBody); const cookies = setCookie.parse(result.headers.getSetCookie()); return { - accessToken: cookies.find(c => c.name === 'sAccessToken')?.value ?? ('' as string), + accessToken: cookies.find(c => c.name === 'sAccessToken')?.value ?? '', user: { id: body.user.id, email: body.user.emails[0], diff --git a/package.json b/package.json index 2608c5942..8d8507f15 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,6 @@ "overrides.ip": "There is no update with fix for ip package, we use fork https://github.com/indutny/node-ip/issues/150#issuecomment-2325961380", "overrides.miniflare@3>undici": "To address CVE: https://github.com/graphql-hive/console/security/dependabot/439", "overrides.tar-fs": "https://github.com/graphql-hive/console/security/dependabot/290", - "overrides.nodemailer@^6.0.0": "supertokens-node override for vulnerable version", "overrides.@types/nodemailer>@aws-sdk/client-sesv2": "@types/nodemailer depends on some AWS stuff that causes the 3.x.x version to stick around. We don't need that dependency. (https://github.com/graphql-hive/console/security/dependabot/436)", "overrides.tar@6.x.x": "address https://github.com/graphql-hive/console/security/dependabot/443", "overrides.diff@<8.0.3": "address https://github.com/graphql-hive/console/security/dependabot/438", @@ -148,7 +147,6 @@ "@tailwindcss/node>tailwindcss": "4.1.18", "@tailwindcss/vite>tailwindcss": "4.1.18", "estree-util-value-to-estree": "^3.3.3", - "nodemailer@^6.0.0": "^7.0.11", "@types/nodemailer>@aws-sdk/client-sesv2": "-", "tar@6.x.x": "^7.5.11", "diff@<8.0.3": "^8.0.3", diff --git a/packages/migrations/src/actions/2024.11.11T00-00-00.supertokens-8.0.ts b/packages/migrations/src/actions/2024.11.11T00-00-00.supertokens-8.0.ts deleted file mode 100644 index 32648155c..000000000 --- a/packages/migrations/src/actions/2024.11.11T00-00-00.supertokens-8.0.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type MigrationExecutor } from '../pg-migrator'; - -export default { - name: '2024.11.11T00-00-00.supertokens-8.0.ts', - run: ({ sql }) => sql` - ALTER TABLE IF EXISTS "supertokens_user_roles" - DROP CONSTRAINT IF EXISTS "supertokens_user_roles_role_fkey"; - `, -} satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.0.ts b/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.0.ts deleted file mode 100644 index 20f3c17e6..000000000 --- a/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.0.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type MigrationExecutor } from '../pg-migrator'; - -export default { - name: '2024.11.12T00-00-00.supertokens-9.0.ts', - run: ({ sql }) => sql` - ALTER TABLE IF EXISTS "supertokens_totp_user_devices" - ADD COLUMN IF NOT EXISTS "created_at" BIGINT default 0; - ALTER TABLE IF EXISTS "totp_user_devices" - ALTER COLUMN "created_at" DROP DEFAULT; - `, -} satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.1.ts b/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.1.ts deleted file mode 100644 index 61f0b58a0..000000000 --- a/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.1.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type MigrationExecutor } from '../pg-migrator'; - -export default { - name: '2024.11.12T00-00-00.supertokens-9.1.ts', - run: ({ sql }) => sql` - ALTER TABLE IF EXISTS "supertokens_tenant_configs" - ADD COLUMN IF NOT EXISTS "is_first_factors_null" BOOLEAN DEFAULT TRUE; - ALTER TABLE IF EXISTS "supertokens_tenant_configs" - ALTER COLUMN "is_first_factors_null" DROP DEFAULT; - `, -} satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.2.ts b/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.2.ts deleted file mode 100644 index 81d3099a8..000000000 --- a/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.2.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type MigrationExecutor } from '../pg-migrator'; - -export default { - name: '2024.11.12T00-00-00.supertokens-9.2.ts', - run: ({ sql }) => sql` - DO $$ - BEGIN - IF (SELECT to_regclass('supertokens_user_last_active') IS NOT null) - THEN - CREATE INDEX IF NOT EXISTS "supertokens_user_last_active_last_active_time_index" - ON "supertokens_user_last_active" ("last_active_time" DESC, "app_id" DESC); - END IF; - END $$; - `, -} satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.3.ts b/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.3.ts deleted file mode 100644 index f64ec1c46..000000000 --- a/packages/migrations/src/actions/2024.11.12T00-00-00.supertokens-9.3.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { type MigrationExecutor } from '../pg-migrator'; - -export default { - name: '2024.11.12T00-00-00.supertokens-9.3.ts', - run: ({ sql }) => sql` - DO $$ - BEGIN - IF (SELECT to_regclass('supertokens_apps') IS NOT null) - THEN - CREATE TABLE IF NOT EXISTS "supertokens_oauth_clients" ( - "app_id" VARCHAR(64), - "client_id" VARCHAR(255) NOT NULL, - "is_client_credentials_only" BOOLEAN NOT NULL, - PRIMARY KEY ("app_id", "client_id"), - FOREIGN KEY("app_id") REFERENCES "supertokens_apps"("app_id") ON DELETE CASCADE - ); - CREATE TABLE IF NOT EXISTS "supertokens_oauth_sessions" ( - "gid" VARCHAR(255), - "app_id" VARCHAR(64) DEFAULT 'public', - "client_id" VARCHAR(255) NOT NULL, - "session_handle" VARCHAR(128), - "external_refresh_token" VARCHAR(255) UNIQUE, - "internal_refresh_token" VARCHAR(255) UNIQUE, - "jti" TEXT NOT NULL, - "exp" BIGINT NOT NULL, - PRIMARY KEY ("gid"), - FOREIGN KEY("app_id", "client_id") REFERENCES "supertokens_oauth_clients"("app_id", "client_id") ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS "supertokens_oauth_session_exp_index" - ON "supertokens_oauth_sessions"("exp" DESC); - CREATE INDEX IF NOT EXISTS "supertokens_oauth_session_external_refresh_token_index" - ON "supertokens_oauth_sessions"("app_id", "external_refresh_token" DESC); - - CREATE TABLE IF NOT EXISTS "supertokens_oauth_m2m_tokens" ( - "app_id" VARCHAR(64) DEFAULT 'public', - "client_id" VARCHAR(255) NOT NULL, - "iat" BIGINT NOT NULL, - "exp" BIGINT NOT NULL, - PRIMARY KEY ("app_id", "client_id", "iat"), - FOREIGN KEY("app_id", "client_id") REFERENCES "supertokens_oauth_clients"("app_id", "client_id") ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS "supertokens_oauth_m2m_token_iat_index" - ON "supertokens_oauth_m2m_tokens"("iat" DESC, "app_id" DESC); - CREATE INDEX IF NOT EXISTS "supertokens_oauth_m2m_token_exp_index" - ON "supertokens_oauth_m2m_tokens"("exp" DESC); - - CREATE TABLE IF NOT EXISTS "supertokens_oauth_logout_challenges" ( - "app_id" VARCHAR(64) DEFAULT 'public', - "challenge" VARCHAR(128) NOT NULL, - "client_id" VARCHAR(255) NOT NULL, - "post_logout_redirect_uri" VARCHAR(1024), - "session_handle" VARCHAR(128), - "state" VARCHAR(128), - "time_created" BIGINT NOT NULL, - PRIMARY KEY ("app_id", "challenge"), - FOREIGN KEY("app_id", "client_id") REFERENCES "supertokens_oauth_clients"("app_id", "client_id") ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS "oauth_logout_challenges_time_created_index" - ON "supertokens_oauth_logout_challenges"("time_created" DESC); - END IF; - END $$; - `, -} satisfies MigrationExecutor; diff --git a/packages/migrations/src/environment.ts b/packages/migrations/src/environment.ts index c6bbc2737..95d3e1b80 100644 --- a/packages/migrations/src/environment.ts +++ b/packages/migrations/src/environment.ts @@ -34,7 +34,6 @@ const EnvironmentModel = zod.object({ .union([zod.literal('1'), zod.literal('0')]) .optional(), GRAPHQL_HIVE_ENVIRONMENT: emptyString(zod.enum(['prod', 'staging', 'dev']).optional()), - SUPERTOKENS_AT_HOME: zod.union([zod.literal('1'), zod.literal('0')]).optional(), }); const PostgresModel = zod.object({ @@ -115,5 +114,4 @@ export const env = { isClickHouseMigrator: base.CLICKHOUSE_MIGRATOR === 'up', isHiveCloud: base.CLICKHOUSE_MIGRATOR_GRAPHQL_HIVE_CLOUD === '1', hiveCloudEnvironment: base.GRAPHQL_HIVE_ENVIRONMENT ?? null, - useSupertokensAtHome: base.SUPERTOKENS_AT_HOME === '1', } as const; diff --git a/packages/migrations/src/run-pg-migrations.ts b/packages/migrations/src/run-pg-migrations.ts index 88c22b4e7..01a8ad486 100644 --- a/packages/migrations/src/run-pg-migrations.ts +++ b/packages/migrations/src/run-pg-migrations.ts @@ -67,7 +67,6 @@ import migration_2024_06_11T10_10_00_ms_teams_webhook from './actions/2024.06.11 import migration_2024_07_16T13_44_00_oidc_only_access from './actions/2024.07.16T13-44-00.oidc-only-access'; import migration_2024_07_17T00_00_00_app_deployments from './actions/2024.07.17T00-00-00.app-deployments'; import migration_2024_07_23T_09_36_00_schema_cleanup_tracker from './actions/2024.07.23T09.36.00.schema-cleanup-tracker'; -import { env } from './environment'; import { runMigrations } from './pg-migrator'; export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: string }) => @@ -143,11 +142,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri migration_2024_07_16T13_44_00_oidc_only_access, migration_2024_07_17T00_00_00_app_deployments, migration_2024_07_23T_09_36_00_schema_cleanup_tracker, - await import('./actions/2024.11.11T00-00-00.supertokens-8.0'), - await import('./actions/2024.11.12T00-00-00.supertokens-9.0'), - await import('./actions/2024.11.12T00-00-00.supertokens-9.1'), - await import('./actions/2024.11.12T00-00-00.supertokens-9.2'), - await import('./actions/2024.11.12T00-00-00.supertokens-9.3'), await import('./actions/2024.12.23T00-00-00.improve-version-index'), await import('./actions/2024.12.24T00-00-00.improve-version-index-2'), await import('./actions/2024.12.27T00.00.00.create-preflight-scripts'), @@ -185,10 +179,8 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri await import('./actions/2026.01.30T00-00-00.account-linking'), await import('./actions/2026.02.06T00-00-00.zendesk-unique'), await import('./actions/2026.01.30T10-00-00.oidc-require-invitation'), + await import('./actions/2026.02.18T00-00-00.ensure-supertokens-tables'), await import('./actions/2026.02.19T00-00-00.saved-filter-permission'), - ...(env.useSupertokensAtHome - ? [await import('./actions/2026.02.18T00-00-00.ensure-supertokens-tables')] - : []), await import('./actions/2026.02.24T00-00-00.proposal-composition'), await import('./actions/2026.02.25T00-00-00.oidc-integration-domains'), ], diff --git a/packages/services/api/package.json b/packages/services/api/package.json index 4132ac478..31e3f5f07 100644 --- a/packages/services/api/package.json +++ b/packages/services/api/package.json @@ -75,7 +75,6 @@ "redlock": "5.0.0-beta.2", "slonik": "30.4.4", "stripe": "17.5.0", - "supertokens-node": "16.7.5", "tslib": "2.8.1", "undici": "7.18.2", "vitest": "4.0.9", diff --git a/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts index 6289ba5b2..61cd50a69 100644 --- a/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts +++ b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts @@ -1,9 +1,7 @@ import c from 'node:crypto'; import { parse as parseCookie } from 'cookie-es'; -import SessionNode from 'supertokens-node/recipe/session/index.js'; import * as zod from 'zod'; import type { FastifyReply, FastifyRequest } from '@hive/service-common'; -import { captureException } from '@sentry/node'; import { AccessError, HiveError, OIDCRequiredError } from '../../../shared/errors'; import { isUUID } from '../../../shared/is-uuid'; import { OIDCIntegrationStore } from '../../oidc-integrations/providers/oidc-integration.store'; @@ -169,7 +167,7 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy { - let session: SessionNode.SessionContainer | undefined; - - try { - session = await SessionNode.getSession(args.req, args.reply, { - sessionRequired: false, - antiCsrfCheck: false, - checkDatabase: true, - }); - args.req.log.debug('Session resolution ended successfully'); - } catch (error) { - args.req.log.debug('Session resolution failed'); - if (SessionNode.Error.isErrorFromSuperTokens(error)) { - if ( - error.type === SessionNode.Error.TRY_REFRESH_TOKEN || - error.type === SessionNode.Error.UNAUTHORISED - ) { - throw new HiveError('Invalid session', { - extensions: { - code: 'NEEDS_REFRESH', - }, - }); - } - } - - args.req.log.error('Error while resolving user'); - console.log(error); - captureException(error); - - throw error; - } - - if (!session) { - args.req.log.debug('No session found'); - return null; - } - - const payload = session.getAccessTokenPayload(); - - const result = SuperTokensSessionPayloadModel.safeParse(payload); - - if (result.success === false) { - args.req.log.error('SuperTokens session payload is invalid'); - args.req.log.debug('SuperTokens session payload: %s', JSON.stringify(payload)); - args.req.log.debug( - 'SuperTokens session parsing errors: %s', - JSON.stringify(result.error.flatten().fieldErrors), - ); - throw new HiveError('Invalid access token provided', { - extensions: { - code: 'UNAUTHENTICATED', - }, - }); - } - - if (result.data.version === '1') { - args.req.log.debug('legacy session detected, require session refresh'); - throw new HiveError('Invalid session.', { - extensions: { - code: 'NEEDS_REFRESH', - }, - }); - } - - return result.data; - } - - private async _verifySuperTokensAtHomeSession( - args: { - req: FastifyRequest; - reply: FastifyReply; - }, - accessTokenKey: AccessTokenKeyContainer, - ): Promise { let session: SessionInfo | null = null; args.req.log.debug('attempt parsing access token from cookie'); @@ -285,7 +210,7 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy` | -| `HIVE_PERSISTED_DOCUMENTS_CDN_ACCESS_KEY` | No (Yes if `HIVE_PERSISTED_DOCUMENTS` is set to `1`) | The access token key for the Hive CDN. | `hv2abcdefg` | -| `LOG_LEVEL` | No | The verbosity of the service logs. One of `trace`, `debug`, `info`, `warn` ,`error`, `fatal` or `silent` | `info` (default) | -| `OPENTELEMETRY_COLLECTOR_ENDPOINT` | No | OpenTelemetry Collector endpoint. The expected traces transport is HTTP (port `4318`). | `http://localhost:4318/v1/traces` | +| Name | Required | Description | Example Value | +| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| `PORT` | **Yes** | The port this service is running on. | `4013` | +| `ENCRYPTION_SECRET` | **Yes** | Secret for encrypting stuff. | `8ebe95cg21c1fee355e9fa32c8c33141` | +| `WEB_APP_URL` | **Yes** | The url of the web app. | `http://127.0.0.1:3000` | +| `GRAPHQL_PUBLIC_ORIGIN` | **Yes** | The origin of the GraphQL server. | `http://127.0.0.1:4013` | +| `TOKENS_ENDPOINT` | **Yes** | The endpoint of the tokens service. | `http://127.0.0.1:6001` | +| `SCHEMA_ENDPOINT` | **Yes** | The endpoint of the schema service. | `http://127.0.0.1:6500` | +| `SCHEMA_POLICY_ENDPOINT` | **No** | The endpoint of the schema policy service. | `http://127.0.0.1:6600` | +| `POSTGRES_SSL` | No | Whether the postgres connection should be established via SSL. | `1` (enabled) or `0` (disabled) | +| `POSTGRES_HOST` | **Yes** | Host of the postgres database | `127.0.0.1` | +| `POSTGRES_PORT` | **Yes** | Port of the postgres database | `5432` | +| `POSTGRES_DB` | **Yes** | Name of the postgres database. | `registry` | +| `POSTGRES_USER` | **Yes** | User name for accessing the postgres database. | `postgres` | +| `POSTGRES_PASSWORD` | No | Password for accessing the postgres database. | `postgres` | +| `CLICKHOUSE_PROTOCOL` | **Yes** | The clickhouse protocol for connecting to the clickhouse instance. | `http` | +| `CLICKHOUSE_HOST` | **Yes** | The host of the clickhouse instance. | `127.0.0.1` | +| `CLICKHOUSE_PORT` | **Yes** | The port of the clickhouse instance | `8123` | +| `CLICKHOUSE_USERNAME` | **Yes** | The username for accessing the clickhouse instance. | `test` | +| `CLICKHOUSE_PASSWORD` | **Yes** | The password for accessing the clickhouse instance. | `test` | +| `CLICKHOUSE_REQUEST_TIMEOUT` | No | Force a request timeout value for ClickHouse operations (in ms) | `30000` | +| `REDIS_HOST` | **Yes** | The host of your redis instance. | `"127.0.0.1"` | +| `REDIS_PORT` | **Yes** | The port of your redis instance. | `6379` | +| `REDIS_PASSWORD` | **Yes** | The password of your redis instance. | `"password"` | +| `REDIS_TLS_ENABLED` | **No** | Enable TLS for redis connection (rediss://). | `"0"` | +| `S3_ENDPOINT` | **Yes** | The S3 endpoint. | `http://localhost:9000` | +| `S3_ACCESS_KEY_ID` | **Yes** | The S3 access key id. | `minioadmin` | +| `S3_SECRET_ACCESS_KEY` | **Yes** | The S3 secret access key. | `minioadmin` | +| `S3_BUCKET_NAME` | **Yes** | The S3 bucket name. | `artifacts` | +| `S3_SESSION_TOKEN` | No | The S3 session token. | `dummytoken` | +| `S3_MIRROR` | No | Whether S3 mirror is enabled | `1` (enabled) or `0` (disabled) | +| `S3_MIRROR_ENDPOINT` | **Yes** | The S3 endpoint. | `http://localhost:9000` | +| `S3_MIRROR_ACCESS_KEY_ID` | **Yes** | The S3 access key id. | `minioadmin` | +| `S3_MIRROR_SECRET_ACCESS_KEY` | **Yes** | The S3 secret access key. | `minioadmin` | +| `S3_MIRROR_BUCKET_NAME` | **Yes** | The S3 bucket name. | `artifacts` | +| `S3_MIRROR_SESSION_TOKEN` | No | The S3 session token. | `dummytoken` | +| `S3_MIRROR_PUBLIC_URL` | No | The public URL of the S3, in case it differs from the `S3_ENDPOINT`. | `http://localhost:8083` | +| `CDN_API` | No | Whether the CDN exposed via API is enabled. | `1` (enabled) or `0` (disabled) | +| `CDN_API_BASE_URL` | No (Yes if `CDN_API` is set to `1`) | The public base url of the API service. | `http://localhost:8082` | +| `CDN_API_KV_BASE_URL` | No (**Optional** if `CDN_API` is set to `1`) | The base URL for the KV for API Provider. Used for scenarios where we cache CDN access. | `https://key-cache.graphql-hive.com` | +| `SUPERTOKENS_API_KEY` | **Yes** [Instructions](https://the-guild.dev/graphql/hive/docs/schema-registry/self-hosting/get-started#running-hive-console) | The key for signing access tokens for user sessions. | string | +| `SUPERTOKENS_REFRESH_TOKEN_KEY` | **Yes** [Instructions](https://the-guild.dev/graphql/hive/docs/schema-registry/self-hosting/get-started#running-hive-console) | The key for signing refresh tokens for user sessions. | string | +| `SUPERTOKENS_RATE_LIMIT` | No (Default value: `1`) | Whether supertokens requests should be rate limited. | `1` (enabled) or `0` (disabled) | +| `SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME` | No (Default value: `CF-Connecting-IP`) | Name of the header to be used for rate limiting. | `CF-Connecting-IP` | +| `AUTH_GITHUB` | No | Whether login via GitHub should be allowed | `1` (enabled) or `0` (disabled) | +| `AUTH_GITHUB_CLIENT_ID` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client ID. | `g6aff8102efda5e1d12e` | +| `AUTH_GITHUB_CLIENT_SECRET` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` | +| `AUTH_GOOGLE` | No | Whether login via Google should be allowed | `1` (enabled) or `0` (disabled) | +| `AUTH_GOOGLE_CLIENT_ID` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client ID. | `g6aff8102efda5e1d12e` | +| `AUTH_GOOGLE_CLIENT_SECRET` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` | +| `AUTH_ORGANIZATION_OIDC` | No | Whether linking a Hive organization to an Open ID Connect provider is allowed. (Default: `0`) | `1` (enabled) or `0` (disabled) | +| `AUTH_OKTA` | No | Whether login via Okta should be allowed | `1` (enabled) or `0` (disabled) | +| `AUTH_OKTA_CLIENT_ENDPOINT` | No (**Yes** if `AUTH_OKTA` is set) | The Okta endpoint. | `https://dev-1234567.okta.com` | +| `AUTH_OKTA_HIDDEN` | No | Whether the Okta login button should be hidden. (Default: `0`) | `1` (enabled) or `0` (disabled) | +| `AUTH_OKTA_CLIENT_ID` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client ID. | `g6aff8102efda5e1d12e` | +| `AUTH_OKTA_CLIENT_SECRET` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` | +| `AUTH_REQUIRE_EMAIL_VERIFICATION` | No | Whether verifying the email address is mandatory. | `1` (enabled) or `0` (disabled) | +| `INTEGRATION_GITHUB` | No | Whether the GitHub integration is enabled | `1` (enabled) or `0` (disabled) | +| `INTEGRATION_GITHUB_GITHUB_APP_ID` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app id. | `123` | +| `INTEGRATION_GITHUB_GITHUB_APP_PRIVATE_KEY` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app private key. | `letmein1` | +| `FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED` | No | Whether app deployments should be enabled for every organization. | `1` (enabled **default**) or `0` (disabled) | +| `FEATURE_FLAGS_SCHEMA_PROPOSALS_ENABLED` | No | Whether schema proposals should be enabled for every organization. | `1` (enabled) or `0` (disabled) | +| `S3_AUDIT_LOG` | No (audit log uses default S3 if not configured) | Whether audit logs should be stored on another S3 bucket than the artifacts. | `1` (enabled) or `0` (disabled) | +| `S3_AUDIT_LOG_ENDPOINT` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 endpoint. | `http://localhost:9000` | +| `S3_AUDIT_LOG_ACCESS_KEY_ID` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 access key id. | `minioadmin` | +| `S3_AUDIT_LOG_SECRET_ACCESS_KEY` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 secret access key. | `minioadmin` | +| `S3_AUDIT_LOG_BUCKET_NAME` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 bucket name. | `artifacts` | +| `S3_AUDIT_LOG_SESSION_TOKEN` | No | The S3 session token. | `dummytoken` | +| `S3_AUDIT_LOG_PUBLIC_URL` | No | The public URL of the S3, in case it differs from the `S3_ENDPOINT`. | `http://localhost:8083` | +| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` | +| `SENTRY` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) | +| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` | +| `PROMETHEUS_METRICS` | No | Whether Prometheus metrics should be enabled | `1` (enabled) or `0` (disabled) | +| `PROMETHEUS_METRICS_LABEL_INSTANCE` | No | The instance label added for the prometheus metrics. | `server` | +| `PROMETHEUS_METRICS_PORT` | No | Port on which prometheus metrics are exposed | Defaults to `10254` | +| `REQUEST_LOGGING` | No | Log http requests | `1` (enabled) or `0` (disabled) | +| `HIVE_PERSISTED_DOCUMENTS` | No | Whether persisted documents should be enabled or disabled | `1` (enabled) or `0` (disabled) | +| `HIVE_PERSISTED_DOCUMENTS_CDN_ENDPOINT` | No (Yes if `HIVE_PERSISTED_DOCUMENTS` is set to `1`) | The endpoint for the Hive persisted documents CDN. | `https://cdn.graphql-hive.com/artifacts/v1/` | +| `HIVE_PERSISTED_DOCUMENTS_CDN_ACCESS_KEY` | No (Yes if `HIVE_PERSISTED_DOCUMENTS` is set to `1`) | The access token key for the Hive CDN. | `hv2abcdefg` | +| `LOG_LEVEL` | No | The verbosity of the service logs. One of `trace`, `debug`, `info`, `warn` ,`error`, `fatal` or `silent` | `info` (default) | +| `OPENTELEMETRY_COLLECTOR_ENDPOINT` | No | OpenTelemetry Collector endpoint. The expected traces transport is HTTP (port `4318`). | `http://localhost:4318/v1/traces` | ## Hive Cloud Configuration diff --git a/packages/services/server/package.json b/packages/services/server/package.json index 2ea9c85c4..9ad119007 100644 --- a/packages/services/server/package.json +++ b/packages/services/server/package.json @@ -53,8 +53,6 @@ "pino-pretty": "11.3.0", "prom-client": "15.1.3", "reflect-metadata": "0.2.2", - "supertokens-js-override": "0.0.4", - "supertokens-node": "16.7.5", "tslib": "2.8.1", "zod": "3.25.76" }, diff --git a/packages/services/server/src/environment.ts b/packages/services/server/src/environment.ts index a69d8a5b9..c73babf69 100644 --- a/packages/services/server/src/environment.ts +++ b/packages/services/server/src/environment.ts @@ -102,24 +102,13 @@ const RedisModel = zod.object({ REDIS_TLS_ENABLED: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), }); -const SuperTokensModel = zod.union([ - zod.object({ - SUPERTOKENS_AT_HOME: emptyString(zod.literal('0').optional()), - SUPERTOKENS_CONNECTION_URI: zod.string().url(), - SUPERTOKENS_API_KEY: zod.string(), - SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), - SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()), - SUPERTOKENS_RATE_LIMIT_BYPASS_KEY: emptyString(zod.string().optional()), - }), - zod.object({ - SUPERTOKENS_AT_HOME: zod.literal('1'), - SUPERTOKENS_REFRESH_TOKEN_KEY: zod.string(), - SUPERTOKENS_ACCESS_TOKEN_KEY: zod.string(), - SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), - SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()), - SUPERTOKENS_RATE_LIMIT_BYPASS_KEY: emptyString(zod.string().optional()), - }), -]); +const SuperTokensModel = zod.object({ + SUPERTOKENS_REFRESH_TOKEN_KEY: zod.string(), + SUPERTOKENS_ACCESS_TOKEN_KEY: zod.string(), + SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), + SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()), + SUPERTOKENS_RATE_LIMIT_BYPASS_KEY: emptyString(zod.string().optional()), +}); const GitHubModel = zod.union([ zod.object({ @@ -446,36 +435,19 @@ export const env = { password: redis.REDIS_PASSWORD ?? '', tlsEnabled: redis.REDIS_TLS_ENABLED === '1', }, - supertokens: - supertokens.SUPERTOKENS_AT_HOME === '1' - ? { - type: 'atHome' as const, - secrets: { - refreshTokenKey: supertokens.SUPERTOKENS_REFRESH_TOKEN_KEY, - accessTokenKey: supertokens.SUPERTOKENS_ACCESS_TOKEN_KEY, + supertokens: { + secrets: { + refreshTokenKey: supertokens.SUPERTOKENS_REFRESH_TOKEN_KEY, + accessTokenKey: supertokens.SUPERTOKENS_ACCESS_TOKEN_KEY, + }, + rateLimit: + supertokens.SUPERTOKENS_RATE_LIMIT === '0' + ? null + : { + ipHeaderName: supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP', + bypassKey: supertokens.SUPERTOKENS_RATE_LIMIT_BYPASS_KEY ?? null, }, - rateLimit: - supertokens.SUPERTOKENS_RATE_LIMIT === '0' - ? null - : { - ipHeaderName: - supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP', - bypassKey: supertokens.SUPERTOKENS_RATE_LIMIT_BYPASS_KEY ?? null, - }, - } - : { - type: 'core' as const, - connectionURI: supertokens.SUPERTOKENS_CONNECTION_URI, - apiKey: supertokens.SUPERTOKENS_API_KEY, - rateLimit: - supertokens.SUPERTOKENS_RATE_LIMIT === '0' - ? null - : { - ipHeaderName: - supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP', - bypassKey: supertokens.SUPERTOKENS_RATE_LIMIT_BYPASS_KEY ?? null, - }, - }, + }, auth: { github: authGithub.AUTH_GITHUB === '1' diff --git a/packages/services/server/src/index.ts b/packages/services/server/src/index.ts index 93c2d92c1..389fbe0cf 100644 --- a/packages/services/server/src/index.ts +++ b/packages/services/server/src/index.ts @@ -1,15 +1,9 @@ #!/usr/bin/env node import got from 'got'; import { GraphQLError, stripIgnoredCharacters } from 'graphql'; -import supertokens from 'supertokens-node'; -import { - errorHandler as supertokensErrorHandler, - plugin as supertokensFastifyPlugin, -} from 'supertokens-node/framework/fastify/index.js'; import cors from '@fastify/cors'; import type { FastifyCorsOptionsDelegateCallback } from '@fastify/cors'; import 'reflect-metadata'; -import { z } from 'zod'; import formDataPlugin from '@fastify/formbody'; import { createRegistry, @@ -60,7 +54,6 @@ import { graphqlHandler } from './graphql-handler'; import { clickHouseElapsedDuration, clickHouseReadDuration } from './metrics'; import { createOtelAuthEndpoint } from './otel-auth-endpoint'; import { createPublicGraphQLHandler } from './public-graphql-handler'; -import { initSupertokens, oidcIdLookup } from './supertokens'; import { registerSupertokensAtHome } from './supertokens-at-home'; class CorsError extends Error { @@ -131,12 +124,6 @@ export async function main() { return res.status(403).send(err.message); } - if (env.supertokens.type === 'core') { - // We can not upgrade Supertokens Node as it removed some APIs we rely on for - // our SSO flow. This the as `any` cast here. - // The code is still compatible and purely a type error. - return supertokensErrorHandler()(err, req, res as any); - } server.log.error(err); return res.status(500); }); @@ -166,9 +153,11 @@ export async function main() { 'graphql-client-name', 'ignore-session', 'x-request-id', - ...(env.supertokens.type === 'atHome' - ? ['rid', 'fdi-version', 'anti-csrf', 'authorization', 'st-auth-mode'] - : supertokens.getAllCORSHeaders()), + 'rid', + 'fdi-version', + 'anti-csrf', + 'authorization', + 'st-auth-mode', ], }); }; @@ -396,10 +385,7 @@ export async function main() { emailVerification: env.auth.requireEmailVerification ? registry.injector.get(EmailVerification) : null, - accessTokenKey: - env.supertokens.type === 'atHome' - ? new AccessTokenKeyContainer(env.supertokens.secrets.accessTokenKey) - : null, + accessTokenKey: new AccessTokenKeyContainer(env.supertokens.secrets.accessTokenKey), oidcIntegrationStore: new OIDCIntegrationStore(storage.pool, redis, logger), }), organizationAccessTokenStrategy, @@ -458,21 +444,7 @@ export async function main() { }); } - if (env.supertokens.type == 'core') { - initSupertokens({ - storage, - crypto, - logger: server.log, - redis, - taskScheduler, - broadcastLog, - }); - } - await server.register(formDataPlugin); - if (env.supertokens.type == 'core') { - await server.register(supertokensFastifyPlugin); - } await registerTRPC(server, { router: internalApiRouter, @@ -529,42 +501,6 @@ export async function main() { }, }); - const oidcIdLookupSchema = z.object({ - slug: z.string({ - required_error: 'Slug is required', - }), - }); - - server.post('/auth-api/oidc-id-lookup', async (req, res) => { - const inputResult = oidcIdLookupSchema.safeParse(req.body); - - if (!inputResult.success) { - captureException(inputResult.error, { - extra: { - path: '/auth-api/oidc-id-lookup', - body: req.body, - }, - }); - void res.status(400).send({ - ok: false, - title: 'Invalid input', - description: 'Failed to resolve SSO information due to invalid input.', - status: 400, - } satisfies Awaited>); - return; - } - - const result = await oidcIdLookup(inputResult.data.slug, storage, req.log); - - if (result.ok) { - void res.status(200).send(result); - return; - } - - void res.status(result.status).send(result); - return; - }); - createOtelAuthEndpoint({ server, authN, @@ -581,18 +517,16 @@ export async function main() { return; }); - if (env.supertokens.type === 'atHome') { - await registerSupertokensAtHome( - server, - storage, - registry.injector.get(TaskScheduler), - registry.injector.get(CryptoProvider), - registry.injector.get(RedisRateLimiter), - registry.injector.get(OAuthCache), - broadcastLog, - env.supertokens.secrets, - ); - } + await registerSupertokensAtHome( + server, + storage, + registry.injector.get(TaskScheduler), + registry.injector.get(CryptoProvider), + registry.injector.get(RedisRateLimiter), + registry.injector.get(OAuthCache), + broadcastLog, + env.supertokens.secrets, + ); if (env.cdn.providers.api !== null) { const s3 = { diff --git a/packages/services/server/src/supertokens-at-home.ts b/packages/services/server/src/supertokens-at-home.ts index 1f622cb6f..1d039a125 100644 --- a/packages/services/server/src/supertokens-at-home.ts +++ b/packages/services/server/src/supertokens-at-home.ts @@ -25,7 +25,8 @@ import { TaskScheduler } from '@hive/workflows/kit'; import { PasswordResetTask } from '@hive/workflows/tasks/password-reset'; import { env } from './environment'; import { createNewSession, validatePassword } from './supertokens-at-home/shared'; -import { type BroadcastOIDCIntegrationLog } from './supertokens/oidc-provider'; + +type BroadcastOIDCIntegrationLog = (oidcOrganizationId: string, message: string) => void; /** * Registers the routes of the Supertokens at Home implementation to a fastify instance. @@ -70,6 +71,57 @@ export async function registerSupertokensAtHome( }); } + const OIDCIdLookupSchema = z.object({ + slug: z.string({ + required_error: 'Slug is required', + }), + }); + + server.post('/auth-api/oidc-id-lookup', async (req, res) => { + if (await rateLimiter.isFastifyRouteRateLimited(req)) { + return res.send({ + ok: false, + title: 'Rate Limited', + description: 'Please try again later.', + status: 400, + }); + } + + req.log.debug('Looking up OIDC integration ID'); + const inputResult = OIDCIdLookupSchema.safeParse(req.body); + + if (!inputResult.success) { + req.log.debug('Invalid body sent. Failed to parse slug.'); + return res.status(400).send({ + ok: false, + title: 'Invalid input', + description: 'Failed to resolve SSO information due to invalid input.', + status: 400, + }); + } + + req.log.debug('Parsed slug (slug=%s)', inputResult.data.slug); + + const oidcId = await storage.getOIDCIntegrationIdForOrganizationSlug({ + slug: inputResult.data.slug, + }); + + if (!oidcId) { + req.log.debug('No SSO integration found (slug=%s)', inputResult.data.slug); + return res.status(404).send({ + ok: false, + title: 'SSO integration not found', + description: 'Your organization lacks an SSO integration or it does not exist.', + status: 404, + }); + } + + return res.status(200).send({ + ok: true, + id: oidcId, + }); + }); + server.route({ url: '/auth-api/signout', method: 'POST', @@ -720,6 +772,13 @@ export async function registerSupertokensAtHome( }); } + if (!z.string().uuid().safeParse(query.data.oidc_id).success) { + return rep.status(200).send({ + status: 'GENERAL_ERROR', + message: 'Something went wrong. Please try again', + }); + } + const oidcIntegration = await storage.getOIDCIntegrationById({ oidcIntegrationId: query.data.oidc_id, }); @@ -735,7 +794,7 @@ export async function registerSupertokensAtHome( const oidClientConfig = new oidClient.Configuration( { - issuer: oidcIntegration.id, + issuer: 'noop', authorization_endpoint: oidcIntegration.authorizationEndpoint, userinfo_endpoint: oidcIntegration.userinfoEndpoint, token_endpoint: oidcIntegration.tokenEndpoint, diff --git a/packages/services/server/src/supertokens.ts b/packages/services/server/src/supertokens.ts deleted file mode 100644 index 4c5d16d48..000000000 --- a/packages/services/server/src/supertokens.ts +++ /dev/null @@ -1,542 +0,0 @@ -import type { FastifyBaseLogger } from 'fastify'; -import type Redis from 'ioredis'; -import { CryptoProvider } from 'packages/services/api/src/modules/shared/providers/crypto'; -import { OverrideableBuilder } from 'supertokens-js-override/lib/build/index.js'; -import supertokens from 'supertokens-node'; -import SessionNode from 'supertokens-node/recipe/session/index.js'; -import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types'; -import ThirdPartyEmailPasswordNode from 'supertokens-node/recipe/thirdpartyemailpassword/index.js'; -import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types'; -import type { TypeInput } from 'supertokens-node/types'; -import zod from 'zod'; -import { type Storage } from '@hive/api'; -import { TaskScheduler } from '@hive/workflows/kit'; -import { PasswordResetTask } from '@hive/workflows/tasks/password-reset'; -import { createInternalApiCaller } from './api'; -import { env } from './environment'; -import { - createOIDCSuperTokensProvider, - describeOIDCSignInError, - getLoggerFromUserContext, - getOIDCSuperTokensOverrides, - type BroadcastOIDCIntegrationLog, -} from './supertokens/oidc-provider'; -import { createThirdPartyEmailPasswordNodeOktaProvider } from './supertokens/okta-provider'; - -const SuperTokensSessionPayloadV2Model = zod.object({ - version: zod.literal('2'), - superTokensUserId: zod.string(), - email: zod.string(), - userId: zod.string(), - oidcIntegrationId: zod.string().nullable(), -}); - -type SuperTokensSessionPayload = zod.TypeOf; - -export const backendConfig = (requirements: { - storage: Storage; - crypto: CryptoProvider; - logger: FastifyBaseLogger; - broadcastLog: BroadcastOIDCIntegrationLog; - redis: Redis; - taskScheduler: TaskScheduler; -}): TypeInput => { - const { logger } = requirements; - - const internalApi = createInternalApiCaller({ - storage: requirements.storage, - crypto: requirements.crypto, - }); - const providers: ProviderInput[] = []; - - if (env.auth.github) { - providers.push({ - config: { - thirdPartyId: 'github', - clients: [ - { - scope: ['read:user', 'user:email'], - clientId: env.auth.github.clientId, - clientSecret: env.auth.github.clientSecret, - }, - ], - }, - }); - } - if (env.auth.google) { - providers.push({ - config: { - thirdPartyId: 'google', - clients: [ - { - clientId: env.auth.google.clientId, - clientSecret: env.auth.google.clientSecret, - scope: [ - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile', - 'openid', - ], - }, - ], - }, - }); - } - - if (env.auth.okta) { - providers.push(createThirdPartyEmailPasswordNodeOktaProvider(env.auth.okta)); - } - - if (env.auth.organizationOIDC) { - providers.push( - createOIDCSuperTokensProvider({ - internalApi, - broadcastLog: requirements.broadcastLog, - }), - ); - } - - logger.info('SuperTokens providers: %s', providers.map(p => p.config.thirdPartyId).join(', ')); - logger.info('SuperTokens websiteDomain: %s', env.hiveServices.webApp.url); - logger.info('SuperTokens apiDomain: %s', env.graphql.origin); - - return { - framework: 'fastify', - supertokens: { - connectionURI: env.supertokens.connectionURI ?? '', - apiKey: env.supertokens.apiKey, - }, - telemetry: false, - appInfo: { - // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo - appName: 'GraphQL Hive', - apiDomain: env.graphql.origin, - websiteDomain: env.hiveServices.webApp.url, - apiBasePath: '/auth-api', - websiteBasePath: '/auth', - }, - recipeList: [ - ThirdPartyEmailPasswordNode.init({ - providers, - signUpFeature: { - formFields: [ - { - id: 'firstName', - // optional because of OIDC integration - optional: true, - }, - { - id: 'lastName', - optional: true, - }, - ], - }, - emailDelivery: { - override: originalImplementation => ({ - ...originalImplementation, - async sendEmail(input) { - if (input.type === 'PASSWORD_RESET') { - await requirements.taskScheduler.scheduleTask(PasswordResetTask, { - user: { - id: input.user.id, - email: input.user.email, - }, - passwordResetLink: input.passwordResetLink, - }); - - return Promise.resolve(); - } - - return Promise.reject(new Error('Unsupported email type.')); - }, - }), - }, - override: composeSuperTokensOverrides([ - getEnsureUserOverrides(internalApi, requirements.redis), - env.auth.organizationOIDC ? getOIDCSuperTokensOverrides() : null, - ]), - }), - SessionNode.init({ - override: { - functions: originalImplementation => ({ - ...originalImplementation, - async createNewSession(input) { - console.log(`Creating a new session for "${input.userId}"`); - const user = await supertokens.getUser(input.userId, input.userContext); - - if (!user) { - console.log(`Failed to find user with id "${input.userId}"`); - throw new Error( - `SuperTokens: Creating a new session failed. Could not find user with id ${input.userId}.`, - ); - } - - const ensureUserResult = await internalApi.ensureUser({ - superTokensUserId: user.id, - email: user.emails[0], - oidcIntegrationId: input.userContext['oidcId'] ?? null, - firstName: null, - lastName: null, - }); - if (!ensureUserResult.ok) { - throw new SessionCreationError(ensureUserResult.reason); - } - - const payload: SuperTokensSessionPayload = { - version: '2', - superTokensUserId: input.userId, - userId: ensureUserResult.user.id, - oidcIntegrationId: input.userContext['oidcId'] ?? null, - email: user.emails[0], - }; - - input.accessTokenPayload = structuredClone(payload); - input.sessionDataInDatabase = structuredClone(payload); - - return originalImplementation.createNewSession(input); - }, - }), - }, - }), - ], - isInServerlessEnv: true, - }; -}; - -function extractIPFromUserContext(userContext: unknown): string { - const defaultIp = (userContext as any)._default.request.original.ip; - if (!env.supertokens?.rateLimit) { - return defaultIp; - } - - return ( - (userContext as any)._default.request.getHeaderValue(env.supertokens.rateLimit.ipHeaderName) ?? - defaultIp - ); -} - -function createRedisRateLimiter(redis: Redis, windowSeconds = 5 * 60, maxRequests = 10) { - async function isRateLimited(action: string, ip: string): Promise { - if (env.supertokens.rateLimit === null) { - return false; - } - - const key = `supertokens-rate-limit:${action}:${ip}`; - const current = await redis.incr(key); - if (current === 1) { - await redis.expire(key, windowSeconds); - } - if (current > maxRequests) { - return true; - } - - return false; - } - - return { isRateLimited }; -} - -const getEnsureUserOverrides = ( - internalApi: ReturnType, - redis: Redis, -): ThirdPartEmailPasswordTypeInput['override'] => ({ - apis: originalImplementation => ({ - ...originalImplementation, - emailPasswordSignUpPOST: async input => { - if (!originalImplementation.emailPasswordSignUpPOST) { - throw Error('emailPasswordSignUpPOST is not available'); - } - - const logger = getLoggerFromUserContext(input.userContext); - const ip = extractIPFromUserContext(input.userContext); - const rateLimiter = createRedisRateLimiter(redis); - - if (await rateLimiter.isRateLimited('sign-up', ip)) { - logger.debug('email password sign up rate limited (ip=%s)', ip); - return { - status: 'GENERAL_ERROR', - message: 'Please try again in a few minutes.', - }; - } - - try { - const response = await originalImplementation.emailPasswordSignUpPOST(input); - - const firstName = input.formFields.find(field => field.id === 'firstName')?.value ?? null; - const lastName = input.formFields.find(field => field.id === 'lastName')?.value ?? null; - - if (response.status === 'SIGN_UP_NOT_ALLOWED') { - return { - status: 'SIGN_UP_NOT_ALLOWED', - reason: 'Sign up not allowed.', - }; - } - - if (response.status === 'OK') { - const result = await internalApi.ensureUser({ - superTokensUserId: response.user.id, - email: response.user.emails[0], - oidcIntegrationId: null, - firstName, - lastName, - }); - - if (!result.ok) { - return { - status: 'SIGN_UP_NOT_ALLOWED', - reason: result.reason, - }; - } - } - - return response; - } catch (e) { - if (e instanceof SessionCreationError) { - return { - status: 'SIGN_UP_NOT_ALLOWED', - reason: e.reason, - }; - } - throw e; - } - }, - async emailPasswordSignInPOST(input) { - if (originalImplementation.emailPasswordSignInPOST === undefined) { - throw Error('Should never come here'); - } - - const logger = getLoggerFromUserContext(input.userContext); - const ip = extractIPFromUserContext(input.userContext); - const rateLimiter = createRedisRateLimiter(redis); - - if (await rateLimiter.isRateLimited('sign-in', ip)) { - logger.debug('email sign in rate limited (ip=%s)', ip); - return { - status: 'GENERAL_ERROR', - message: 'Please try again in a few minutes.', - }; - } - - try { - const response = await originalImplementation.emailPasswordSignInPOST(input); - - if (response.status === 'SIGN_IN_NOT_ALLOWED') { - return { - status: 'SIGN_IN_NOT_ALLOWED', - reason: 'Sign in not allowed.', - }; - } - - if (response.status === 'OK') { - const result = await internalApi.ensureUser({ - superTokensUserId: response.user.id, - email: response.user.emails[0], - oidcIntegrationId: null, - // They are not available during sign in. - firstName: null, - lastName: null, - }); - - if (!result.ok) { - return { - status: 'SIGN_IN_NOT_ALLOWED', - reason: result.reason, - }; - } - } - - return response; - } catch (e) { - if (e instanceof SessionCreationError) { - return { - status: 'SIGN_IN_NOT_ALLOWED', - reason: e.reason, - }; - } - throw e; - } - }, - async thirdPartySignInUpPOST(input) { - if (originalImplementation.thirdPartySignInUpPOST === undefined) { - throw Error('Should never come here'); - } - - function extractOidcId(args: typeof input) { - if (input.provider.id === 'oidc') { - const oidcId: unknown = args.userContext['oidcId']; - if (typeof oidcId === 'string') { - return oidcId; - } - } - return null; - } - - try { - const response = await originalImplementation.thirdPartySignInUpPOST(input); - - if (response.status === 'SIGN_IN_UP_NOT_ALLOWED') { - return { - status: 'SIGN_IN_UP_NOT_ALLOWED', - reason: 'Sign in not allowed.', - }; - } - - if (response.status === 'OK') { - const result = await internalApi.ensureUser({ - superTokensUserId: response.user.id, - email: response.user.emails[0], - oidcIntegrationId: extractOidcId(input), - // TODO: should we somehow extract the first and last name from the third party provider? - firstName: null, - lastName: null, - }); - - if (!result.ok) { - return { - status: 'SIGN_IN_UP_NOT_ALLOWED', - reason: result.reason, - }; - } - } - - return response; - } catch (e) { - if (e instanceof SessionCreationError) { - return { - status: 'SIGN_IN_UP_NOT_ALLOWED', - reason: e.reason, - }; - } - if (input.provider.id === 'oidc') { - const logger = getLoggerFromUserContext(input.userContext); - logger.error(e, 'OIDC sign-in/sign-up failed'); - return { - status: 'GENERAL_ERROR' as const, - message: describeOIDCSignInError(e), - }; - } - throw e; - } - }, - async passwordResetPOST(input) { - const logger = getLoggerFromUserContext(input.userContext); - const ip = extractIPFromUserContext(input.userContext); - const rateLimiter = createRedisRateLimiter(redis); - - if (await rateLimiter.isRateLimited('password-reset', ip)) { - logger.debug('password reset rate limited (ip=%s)', ip); - return { - status: 'GENERAL_ERROR', - message: 'Please try again in a few minutes.', - }; - } - - const result = await originalImplementation.passwordResetPOST!(input); - - // For security reasons we revoke all sessions when a password reset is performed. - if (result.status === 'OK' && result.user) { - await SessionNode.revokeAllSessionsForUser(result.user.id); - } - - return result; - }, - }), -}); - -const bindObjectFunctions = ( - obj: T, - bindTo: any, -) => { - return Object.fromEntries( - Object.entries(obj).map(([key, value]) => [key, value?.bind(bindTo)]), - ) as T; -}; - -/** - * Utility function for composing multiple (dynamic SuperTokens overrides). - */ -const composeSuperTokensOverrides = ( - overrides: Array, -) => ({ - apis( - originalImplementation: ReturnType< - Exclude['apis'], undefined> - >, - builder: OverrideableBuilder | undefined, - ) { - let impl = originalImplementation; - for (const override of overrides) { - if (typeof override?.apis === 'function') { - impl = bindObjectFunctions(override.apis(impl, builder), originalImplementation); - } - } - return impl; - }, - functions( - originalImplementation: ReturnType< - Exclude< - Exclude['functions'], - undefined - > - >, - ) { - let impl = originalImplementation; - for (const override of overrides) { - if (typeof override?.functions === 'function') { - impl = bindObjectFunctions(override.functions(impl), originalImplementation); - } - } - return impl; - }, -}); - -export function initSupertokens(requirements: { - storage: Storage; - crypto: CryptoProvider; - logger: FastifyBaseLogger; - broadcastLog: BroadcastOIDCIntegrationLog; - redis: Redis; - taskScheduler: TaskScheduler; -}) { - supertokens.init(backendConfig(requirements)); -} - -type OidcIdLookupResponse = - | { - ok: true; - id: string; - } - | { - ok: false; - title: string; - description: string; - status: number; - }; - -export async function oidcIdLookup( - slug: string, - storage: Storage, - logger: FastifyBaseLogger, -): Promise { - logger.debug('Looking up OIDC integration ID for organization %s', slug); - const oidcId = await storage.getOIDCIntegrationIdForOrganizationSlug({ slug }); - - if (!oidcId) { - return { - ok: false, - title: 'SSO integration not found', - description: 'Your organization lacks an SSO integration or it does not exist.', - status: 404, - }; - } - - return { - ok: true, - id: oidcId, - }; -} - -class SessionCreationError extends Error { - constructor(public reason: string) { - super(reason); - } -} diff --git a/packages/services/server/src/supertokens/oidc-provider.test.ts b/packages/services/server/src/supertokens/oidc-provider.test.ts deleted file mode 100644 index 1c453796b..000000000 --- a/packages/services/server/src/supertokens/oidc-provider.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { describeOIDCSignInError } from './oidc-provider'; - -describe('describeOIDCSignInError', () => { - test('invalid_client error (e.g. expired client secret)', () => { - const error = new Error( - 'Received response with status 401 and body {"error":"invalid_client","error_description":"AAD2SKSASFSLKAF: The provided client secret keys for app \'369sdsds1-8513-4ssa-ae64-292942jsjs\' are expired."}', - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('invalid client credentials'); - expect(message).toContain('client secret has expired'); - expect(message).not.toContain('AAD2SKSASFSLKAF'); - expect(message).not.toContain('369sdsds1-8513-4ssa-ae64-292942jsjs'); - }); - - test('invalid_grant error (e.g. expired authorization code)', () => { - const error = new Error( - 'Received response with status 400 and body {"error":"invalid_grant","error_description":"The authorization code has expired."}', - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('authorization code has expired'); - expect(message).toContain('try signing in again'); - }); - - test('unauthorized_client error', () => { - const error = new Error( - 'Received response with status 403 and body {"error":"unauthorized_client","error_description":"The client is not authorized."}', - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('rejected the client authorization'); - expect(message).toContain('OIDC integration configuration'); - }); - - test('invalid_request error', () => { - const error = new Error( - 'Received response with status 400 and body {"error":"invalid_request","error_description":"The request is missing a required parameter."}', - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('rejected the token request as malformed'); - expect(message).toContain('token endpoint URL'); - }); - - test('unsupported_grant_type error', () => { - const error = new Error( - 'Received response with status 400 and body {"error":"unsupported_grant_type","error_description":"The authorization grant type is not supported."}', - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('does not support the authorization code grant type'); - }); - - test('invalid_scope error', () => { - const error = new Error( - 'Received response with status 400 and body {"error":"invalid_scope","error_description":"The requested scope is invalid."}', - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('rejected the requested scopes'); - expect(message).toContain('additional scopes'); - }); - - test('network error: ECONNREFUSED', () => { - const error = new Error( - 'request to https://login.example.com/token failed, reason: connect ECONNREFUSED 127.0.0.1:443', - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('Could not connect'); - expect(message).toContain('endpoint URLs'); - }); - - test('network error: ENOTFOUND', () => { - const error = new Error( - 'request to https://nonexistent.example.com/token failed, reason: getaddrinfo ENOTFOUND nonexistent.example.com', - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('Could not connect'); - }); - - test('network error: ETIMEDOUT', () => { - const error = new Error( - 'request to https://slow.example.com/token failed, reason: connect ETIMEDOUT', - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('Could not connect'); - }); - - test('network error: fetch failed', () => { - const error = new TypeError('fetch failed'); - const message = describeOIDCSignInError(error); - expect(message).toContain('Could not connect'); - }); - - test('OIDC integration not found', () => { - const error = new Error('Could not find OIDC integration.'); - const message = describeOIDCSignInError(error); - expect(message).toContain('could not be found'); - expect(message).toContain('contact your organization administrator'); - }); - - test('userinfo endpoint returned non-200 status', () => { - const error = new Error( - "Received invalid status code. Could not retrieve user's profile info.", - ); - const message = describeOIDCSignInError(error); - expect(message).toContain('user info endpoint returned an error'); - expect(message).toContain('verify the user info endpoint URL'); - }); - - test('userinfo endpoint returned non-JSON response', () => { - const error = new Error('Could not parse JSON response.'); - const message = describeOIDCSignInError(error); - expect(message).toContain('returned an invalid response'); - expect(message).toContain('verify the user info endpoint URL'); - }); - - test('userinfo endpoint missing required fields (sub, email)', () => { - const error = new Error('Could not parse profile info.'); - const message = describeOIDCSignInError(error); - expect(message).toContain('did not return the required fields'); - expect(message).toContain('sub, email'); - }); - - test('unknown error returns generic message', () => { - const error = new Error('Something completely unexpected happened'); - const message = describeOIDCSignInError(error); - expect(message).toContain('unexpected error'); - expect(message).toContain('verify your OIDC integration configuration'); - }); - - test('non-Error value is handled', () => { - const message = describeOIDCSignInError('string error with invalid_client'); - expect(message).toContain('invalid client credentials'); - }); - - test('no sensitive information is leaked in any branch', () => { - const sensitiveError = new Error( - 'Received response with status 401 and body {"error":"invalid_client","error_description":"AADAASJAD213122: The provided client secret keys for app \'3693bbf1-8513-4cda-ae64-77e3ca237f17\' are expired. Visit the Azure portal to create new keys for your app: https://aka.ms/NewClientSecret Trace ID: b8b7152f-4489-46ed-8b78-11ad45520300 Correlation ID: 45c48f07-0191-431d-8a19-ba8319a7cd18"}', - ); - const message = describeOIDCSignInError(sensitiveError); - expect(message).not.toContain('3693bbf1'); - expect(message).not.toContain('b8b7152f'); - expect(message).not.toContain('45c48f07'); - expect(message).not.toContain('aka.ms'); - expect(message).not.toContain('Trace ID'); - expect(message).not.toContain('Correlation ID'); - }); -}); diff --git a/packages/services/server/src/supertokens/oidc-provider.ts b/packages/services/server/src/supertokens/oidc-provider.ts deleted file mode 100644 index 0ef9684f5..000000000 --- a/packages/services/server/src/supertokens/oidc-provider.ts +++ /dev/null @@ -1,377 +0,0 @@ -import type { FastifyBaseLogger } from 'fastify'; -import type { FastifyRequest } from 'supertokens-node/lib/build/framework/fastify/framework'; -import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types'; -import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types'; -import zod from 'zod'; -import { createInternalApiCaller } from '../api'; - -const couldNotResolveOidcIntegrationSymbol = Symbol('could_not_resolve_oidc_integration'); - -type InternalApiCaller = ReturnType; - -export const getOIDCSuperTokensOverrides = (): ThirdPartEmailPasswordTypeInput['override'] => ({ - apis(originalImplementation) { - return { - ...originalImplementation, - async authorisationUrlGET(input) { - if (input.userContext?.[couldNotResolveOidcIntegrationSymbol] === true) { - return { - status: 'GENERAL_ERROR', - message: 'Could not find OIDC integration.', - }; - } - - return originalImplementation.authorisationUrlGET!(input); - }, - }; - }, -}); - -export type BroadcastOIDCIntegrationLog = (oidcId: string, message: string) => void; - -export function getLoggerFromUserContext(userContext: unknown): FastifyBaseLogger { - return (userContext as any)._default.request.request.log; -} - -export const createOIDCSuperTokensProvider = (args: { - internalApi: InternalApiCaller; - broadcastLog: BroadcastOIDCIntegrationLog; -}): ProviderInput => ({ - config: { - thirdPartyId: 'oidc', - }, - override(originalImplementation) { - return { - ...originalImplementation, - async getConfigForClientType(input) { - const logger = getLoggerFromUserContext(input.userContext); - logger.info('resolve config for OIDC provider.'); - const config = await getOIDCConfigFromInput(args.internalApi, logger, input); - if (!config) { - // In the next step the override `authorisationUrlGET` from `getOIDCSuperTokensOverrides` is called. - // We use the user context to return a `GENERAL_ERROR` with a human readable message. - // We cannot return an error here (except an "Unexpected error"), so we also need to return fake dat - input.userContext[couldNotResolveOidcIntegrationSymbol] = true; - - return { - thirdPartyId: 'oidc', - get clientId(): string { - throw new Error('Noop value accessed.'); - }, - }; - } - - return { - thirdPartyId: 'oidc', - clientId: config.clientId, - clientSecret: config.clientSecret, - authorizationEndpoint: config.authorizationEndpoint, - userInfoEndpoint: config.userinfoEndpoint, - tokenEndpoint: config.tokenEndpoint, - scope: ['openid', 'email', ...config.additionalScopes], - }; - }, - - async getAuthorisationRedirectURL(input) { - const logger = getLoggerFromUserContext(input.userContext); - logger.info('resolve authorization redirect url of OIDC provider.'); - const oidcConfig = await getOIDCConfigFromInput(args.internalApi, logger, input); - - if (!oidcConfig) { - // This case should never be reached (guarded by getConfigForClientType). - // We still have it for security reasons. - throw new Error('Could not find OIDC integration.'); - } - - const authorizationRedirectUrl = - await originalImplementation.getAuthorisationRedirectURL(input); - - const url = new URL(authorizationRedirectUrl.urlWithQueryParams); - url.searchParams.set('state', oidcConfig.id); - - const urlWithQueryParams = url.toString(); - - args.broadcastLog(oidcConfig.id, `redirect client to oauth provider ${urlWithQueryParams}`); - - return { - ...authorizationRedirectUrl, - urlWithQueryParams, - }; - }, - - async exchangeAuthCodeForOAuthTokens(input) { - const logger = getLoggerFromUserContext(input.userContext); - const config = await getOIDCConfigFromInput(args.internalApi, logger, input); - if (!config) { - // This case should never be reached (guarded by getConfigForClientType). - // We still have it for security reasons. - throw new Error('Could not find OIDC integration.'); - } - - logger.info('exchange auth code for oauth token (oidcId=%s)', config.id); - - args.broadcastLog( - config.id, - `attempt exchanging auth code for auth tokens on endpoint ${config.tokenEndpoint}`, - ); - - try { - // TODO: we should probably have our own custom implementation of this that uses fetch API. - // that way we can also do timeouts, retries and more detailed logging. - const result = await originalImplementation.exchangeAuthCodeForOAuthTokens(input); - args.broadcastLog( - config.id, - `successfully exchanged auth code for tokens on endpoint ${config.tokenEndpoint}`, - ); - return result; - } catch (error) { - if (error instanceof Error) { - args.broadcastLog( - config.id, - `error while exchanging auth code for tokens on endpoint ${config.tokenEndpoint}: ${error.message}`, - ); - } - throw error; - } - }, - - async getUserInfo(input) { - const logger = getLoggerFromUserContext(input.userContext); - logger.info('retrieve profile info from OIDC provider'); - const config = await getOIDCConfigFromInput(args.internalApi, logger, input); - if (!config) { - // This case should never be reached (guarded by getConfigForClientType). - // We still have it for security reasons. - throw new Error('Could not find OIDC integration.'); - } - - logger.info('fetch info for OIDC provider (oidcId=%s)', config.id); - - args.broadcastLog( - config.id, - `attempt fetching user info from endpoint with timeout 10 seconds ${config.userinfoEndpoint}`, - ); - - const abortController = new AbortController(); - - const timeout = setTimeout(() => { - abortController.abort(); - args.broadcastLog( - config.id, - `failed fetching user info from endpoint ${config.userinfoEndpoint}. Request timed out.`, - ); - }, 10_000); - - const tokenResponse = OIDCTokenSchema.parse(input.oAuthTokens); - const response = await fetch(config.userinfoEndpoint, { - headers: { - authorization: `Bearer ${tokenResponse.access_token}`, - accept: 'application/json', - 'content-type': 'application/json', - }, - signal: abortController.signal, - }); - - if (response.status !== 200) { - clearTimeout(timeout); - logger.info('received invalid status code (oidcId=%s)', config.id); - args.broadcastLog( - config.id, - `failed fetching user info from endpoint "${config.userinfoEndpoint}". Received status code ${response.status}. Expected 200.`, - ); - throw new Error("Received invalid status code. Could not retrieve user's profile info."); - } - - const body = await response.text(); - clearTimeout(timeout); - - let rawData: unknown; - - try { - rawData = JSON.parse(body); - } catch (err) { - logger.error('Could not parse JSON response from OIDC provider (oidcId=%s)', config.id); - if (err instanceof Error) { - args.broadcastLog( - config.id, - `failed parsing user info request body for response from user info endpoint "${config.userinfoEndpoint}". Error: ${err.message}.`, - ); - } - throw new Error('Could not parse JSON response.'); - } - - logger.info('retrieved profile info for provider (oidcId=%s)', config.id); - - const dataParseResult = OIDCProfileInfoSchema.safeParse(rawData); - - if (!dataParseResult.success) { - logger.error('Could not parse profile info for OIDC provider (oidcId=%s)', config.id); - logger.error('Raw data: %s', JSON.stringify(rawData)); - logger.error('Error: %s', JSON.stringify(dataParseResult.error)); - for (const issue of dataParseResult.error.issues) { - logger.debug('Issue: %s', JSON.stringify(issue)); - } - args.broadcastLog( - config.id, - `failed validating user info request body for response from user info endpoint "${config.userinfoEndpoint}". Issues: ${JSON.stringify(dataParseResult.error.issues)}`, - ); - - throw new Error('Could not parse profile info.'); - } - - args.broadcastLog( - config.id, - `successfully parsed user info request body for response from user info endpoint "${config.userinfoEndpoint}".`, - ); - - const profile = dataParseResult.data; - - // Set the oidcId to the user context so it can be used in `thirdPartySignInUpPOST` for linking the user account to the OIDC integration. - input.userContext.oidcId = config.id; - - return { - thirdPartyUserId: `${config.id}-${profile.sub}`, - email: { - id: profile.email, - isVerified: true, - }, - rawUserInfoFromProvider: { - fromIdTokenPayload: undefined, - fromUserInfoAPI: undefined, - }, - }; - }, - }; - }, -}); - -type OIDCConfig = { - id: string; - clientId: string; - clientSecret: string; - tokenEndpoint: string; - userinfoEndpoint: string; - authorizationEndpoint: string; - additionalScopes: string[]; -}; - -const OIDCProfileInfoSchema = zod.object({ - sub: zod.string(), - email: zod.string().email(), -}); - -const OIDCTokenSchema = zod.object({ access_token: zod.string() }); - -const getOIDCIdFromInput = (input: { userContext: any }, logger: FastifyBaseLogger): string => { - const fastifyRequest = input.userContext._default.request as FastifyRequest; - const originalUrl = 'http://localhost' + fastifyRequest.getOriginalURL(); - const oidcId = new URL(originalUrl).searchParams.get('oidc_id'); - - if (typeof oidcId !== 'string') { - logger.error('Invalid OIDC ID sent from client: %s', oidcId); - throw new Error('Invalid OIDC ID sent from client.'); - } - - return oidcId; -}; - -const configCache = new WeakMap(); - -/** - * Get cached OIDC config from the supertokens input. - */ -async function getOIDCConfigFromInput( - internalApi: InternalApiCaller, - logger: FastifyBaseLogger, - input: { userContext: any }, -) { - const fastifyRequest = input.userContext._default.request as FastifyRequest; - if (configCache.has(fastifyRequest)) { - return configCache.get(fastifyRequest) ?? null; - } - - const oidcIntegrationId = getOIDCIdFromInput(input, logger); - const config = await fetchOIDCConfig(internalApi, logger, oidcIntegrationId); - if (!config) { - configCache.set(fastifyRequest, null); - logger.error('Could not find OIDC integration (oidcId: %s)', oidcIntegrationId); - return null; - } - const resolvedConfig = { oidcIntegrationId, ...config }; - configCache.set(fastifyRequest, resolvedConfig); - - return resolvedConfig; -} - -/** - * Classify an OIDC sign-in error into a user-safe description. - * Avoids leaking sensitive details (app IDs, trace IDs, internal URLs) - * while still pointing administrators toward the likely cause. - */ -export function describeOIDCSignInError(error: unknown): string { - const message = error instanceof Error ? error.message : String(error); - - if (message.includes('invalid_client')) { - return 'Authentication with your OIDC provider failed due to invalid client credentials. This commonly happens when the client secret has expired or the client ID is incorrect. Please review your OIDC integration settings.'; - } - - if (message.includes('invalid_grant')) { - return 'The authorization could not be completed. This can happen if the authorization code has expired. Please try signing in again.'; - } - - if (message.includes('unauthorized_client')) { - return 'Your OIDC provider rejected the client authorization. Please verify your OIDC integration configuration.'; - } - - if (message.includes('invalid_request')) { - return 'Your OIDC provider rejected the token request as malformed. This may indicate a misconfigured token endpoint URL. Please review your OIDC integration settings.'; - } - - if (message.includes('unsupported_grant_type')) { - return 'Your OIDC provider does not support the authorization code grant type. Please verify the provider supports the OAuth 2.0 authorization code flow.'; - } - - if (message.includes('invalid_scope')) { - return 'Your OIDC provider rejected the requested scopes. Please review the additional scopes configured in your OIDC integration settings.'; - } - - if (message.includes('Could not find OIDC integration')) { - return 'The OIDC integration could not be found. It may have been removed or misconfigured. Please contact your organization administrator.'; - } - - if (message.includes("Could not retrieve user's profile info")) { - return "Your OIDC provider's user info endpoint returned an error. Please verify the user info endpoint URL in your OIDC integration settings is correct."; - } - - if (message.includes('Could not parse JSON response')) { - return "Your OIDC provider's user info endpoint returned an invalid response. Please verify the user info endpoint URL in your OIDC integration settings is correct."; - } - - if (message.includes('Could not parse profile info')) { - return "Your OIDC provider's user info endpoint did not return the required fields (sub, email). Please verify your OIDC provider is configured to include these claims."; - } - - if ( - message.includes('ECONNREFUSED') || - message.includes('ENOTFOUND') || - message.includes('ETIMEDOUT') || - message.includes('fetch failed') - ) { - return 'Could not connect to your OIDC provider. Please verify the endpoint URLs in your OIDC integration settings are correct and the server is accessible.'; - } - - return 'An unexpected error occurred while authenticating with your OIDC provider. Please verify your OIDC integration configuration or contact your administrator.'; -} - -const fetchOIDCConfig = async ( - internalApi: InternalApiCaller, - logger: FastifyBaseLogger, - oidcIntegrationId: string, -): Promise => { - const result = await internalApi.getOIDCIntegrationById({ oidcIntegrationId }); - if (result === null) { - logger.error('OIDC integration not found. (oidcId=%s)', oidcIntegrationId); - return null; - } - return result; -}; diff --git a/packages/services/server/src/supertokens/okta-provider.ts b/packages/services/server/src/supertokens/okta-provider.ts deleted file mode 100644 index 3d6030606..000000000 --- a/packages/services/server/src/supertokens/okta-provider.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types'; -import zod from 'zod'; -import { env } from '../environment'; - -type OktaConfig = Exclude<(typeof env)['auth']['okta'], null>; - -/** - * Custom (server) provider for SuperTokens in order to allow Okta users to sign in. - */ -export const createThirdPartyEmailPasswordNodeOktaProvider = ( - config: OktaConfig, -): ProviderInput => { - return { - config: { - thirdPartyId: 'okta', - clients: [ - { - clientId: config.clientId, - clientSecret: config.clientSecret, - scope: ['openid', 'email', 'profile', 'okta.users.read.self'], - }, - ], - authorizationEndpoint: `${config.endpoint}/oauth2/v1/authorize`, - tokenEndpoint: `${config.endpoint}/oauth2/v1/token`, - }, - override(originalImplementation) { - return { - ...originalImplementation, - async getUserInfo(input) { - const data = OktaAccessTokenResponseModel.parse(input.oAuthTokens); - const userData = await fetchOktaProfile(config, data.access_token); - - return { - thirdPartyUserId: userData.id, - email: { - id: userData.profile.email, - isVerified: true, - }, - rawUserInfoFromProvider: { - fromIdTokenPayload: undefined, - fromUserInfoAPI: undefined, - }, - }; - }, - }; - }, - }; -}; - -const OktaAccessTokenResponseModel = zod.object({ - access_token: zod.string(), -}); - -const OktaProfileModel = zod.object({ - id: zod.string(), - profile: zod.object({ - email: zod.string(), - }), -}); - -async function fetchOktaProfile(config: OktaConfig, accessToken: string) { - const response = await fetch(`${config.endpoint}/api/v1/users/me`, { - method: 'GET', - headers: { - 'content-type': 'application/json', - accept: 'application/json', - authorization: `Bearer ${accessToken}`, - }, - }); - - if (response.status !== 200) { - throw new Error(`Unexpected status code from Okta API: ${response.status}`); - } - - const json = await response.json(); - return OktaProfileModel.parse(json); -} diff --git a/packages/web/docs/src/content/schema-registry/self-hosting/get-started.mdx b/packages/web/docs/src/content/schema-registry/self-hosting/get-started.mdx index 1ea7f2738..dc648659e 100644 --- a/packages/web/docs/src/content/schema-registry/self-hosting/get-started.mdx +++ b/packages/web/docs/src/content/schema-registry/self-hosting/get-started.mdx @@ -43,7 +43,6 @@ components: [Azure Event Hubs](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-about)) - [ClickHouse database](https://clickhouse.com/) - [Redis](https://redis.io/) -- [SuperTokens instance](https://supertokens.com/) - [S3 storage](https://aws.amazon.com/s3/) (or any other S3-compatible solution, like [minio](https://min.io/) or [CloudFlare R2](https://www.cloudflare.com/developer-platform/r2/)) - Hive microservices @@ -93,7 +92,6 @@ The following diagram shows the architecture of a self-hosted Hive Console insta #### Microservices -- **SuperTokens**: an open-source project used for authentication and authorization. - `webapp`: the main Hive Console web application. - `server`: the main Hive Console server, responsible for serving the GraphQL API and orchestrating calls to other services. @@ -192,7 +190,6 @@ export DOCKER_TAG=":9.7.1" # Pin this to an exact version export HIVE_ENCRYPTION_SECRET=$(openssl rand -hex 16) export HIVE_APP_BASE_URL="http://localhost:8080" export HIVE_EMAIL_FROM="no-reply@graphql-hive.com" -export SUPERTOKENS_API_KEY=$(openssl rand -hex 16) export CLICKHOUSE_USER="clickhouse" export CLICKHOUSE_PASSWORD=$(openssl rand -hex 16) export REDIS_PASSWORD=$(openssl rand -hex 16) @@ -202,6 +199,13 @@ export POSTGRES_DB="registry" export MINIO_ROOT_USER="minioadmin" export MINIO_ROOT_PASSWORD="minioadmin" export CDN_AUTH_PRIVATE_KEY=$(openssl rand -hex 16) +export SUPERTOKENS_REFRESH_TOKEN_KEY=$(echo "1000:$(openssl rand -hex 64):$(openssl rand -hex 64)") +KEY_NAME=$(uuidgen) +PRIVATE_KEY_PEM=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048) +PUBLIC_KEY_PEM=$(echo "$PRIVATE_KEY_PEM" | openssl rsa -pubout) +PRIVATE_KEY_DATA=$(echo "$PRIVATE_KEY_PEM" | awk 'NF {if (NR!=1 && $0!~/-----END/) print}' | tr -d '\n') +PUBLIC_KEY_DATA=$(echo "$PUBLIC_KEY_PEM" | awk 'NF {if (NR!=1 && $0!~/-----END/) print}' | tr -d '\n') +export SUPERTOKENS_ACCESS_TOKEN_KEY=$(echo "${KEY_NAME}|${PUBLIC_KEY_DATA}|${PRIVATE_KEY_DATA}") ``` @@ -390,8 +394,6 @@ After doing your first testing with Hive Console you should consider the followi new releases and updates. - Set up a weekly reminder for updating your Hive Console instance to the latest version and applying maintenance. -- Get yourself familiar with SuperTokens and follow their changelogs in order to keep your - SuperTokens instance up-to-date. - In order to use longer retention periods e.g. for conditional breaking changes or schema explorer overviews do the following: Open the postgres database, go to the table `organization`, find your organization and change `plan_name` to ENTERPRISE, `limit_operations_monthly` to 0 and diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da3c6a310..703dab7c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,6 @@ overrides: '@tailwindcss/node>tailwindcss': 4.1.18 '@tailwindcss/vite>tailwindcss': 4.1.18 estree-util-value-to-estree: ^3.3.3 - nodemailer@^6.0.0: ^7.0.11 '@types/nodemailer>@aws-sdk/client-sesv2': '-' tar@6.x.x: ^7.5.11 diff@<8.0.3: ^8.0.3 @@ -1225,9 +1224,6 @@ importers: stripe: specifier: 17.5.0 version: 17.5.0 - supertokens-node: - specifier: 16.7.5 - version: 16.7.5(encoding@0.1.13) tslib: specifier: 2.8.1 version: 2.8.1 @@ -1649,12 +1645,6 @@ importers: reflect-metadata: specifier: 0.2.2 version: 0.2.2 - supertokens-js-override: - specifier: 0.0.4 - version: 0.0.4 - supertokens-node: - specifier: 16.7.5 - version: 16.7.5(encoding@0.1.13) tslib: specifier: 2.8.1 version: 2.8.1 @@ -12131,9 +12121,6 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - cross-fetch@3.1.8: - resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} - cross-inspect@1.0.1: resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} engines: {node: '>=16.0.0'} @@ -14349,10 +14336,6 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} - inflation@2.1.0: - resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==} - engines: {node: '>= 0.8.0'} - inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -15109,9 +15092,6 @@ packages: libmime@2.1.3: resolution: {integrity: sha512-ABr2f4O+K99sypmkF/yPz2aXxUFHEZzv+iUkxItCeKZWHHXdQPpDXd6rV1kBBwL4PserzLU09EIzJ2lxC9hPfQ==} - libphonenumber-js@1.12.17: - resolution: {integrity: sha512-bsxi8FoceAYR/bjHcLYc2ShJ/aVAzo5jaxAYiMHF0BD+NTp47405CGuPNKYpw+lHadN9k/ClFGc9X5vaZswIrA==} - libqp@1.1.0: resolution: {integrity: sha512-4Rgfa0hZpG++t1Vi2IiqXG9Ad1ig4QTmtuZF946QJP4bPqOYC78ixUXgz5TW/wE7lNaNKlplSYTxQ+fR2KZ0EA==} @@ -16955,9 +16935,6 @@ packages: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} - pkce-challenge@3.1.0: - resolution: {integrity: sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==} - pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -17341,9 +17318,6 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - psl@1.8.0: - resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} - pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -17376,9 +17350,6 @@ packages: resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==} engines: {node: '>=18'} - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -17872,9 +17843,6 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -18108,10 +18076,6 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - scmp@2.1.0: - resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==} - deprecated: Just use Node.js's crypto.timingSafeEqual() - scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} @@ -18693,9 +18657,6 @@ packages: supertokens-js-override@0.0.4: resolution: {integrity: sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==} - supertokens-node@16.7.5: - resolution: {integrity: sha512-xrVwi0GfgLIqHKaWXEp7fCgmVQ2wTiJTHG2CQGLkGMvrO9dUcxY+H6qozn3EvyXxbkm1BmflYD8N7ILNSIbR1g==} - supertokens-web-js@0.9.0: resolution: {integrity: sha512-7DucVUWxImrcjckza0oW6tkPfMzScj8V/qiQNZeUT/EfCqIbslNSO8holBHc9Eykc0vG/CC0d9ne5TRhAmRcxg==} @@ -19165,10 +19126,6 @@ packages: tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - twilio@4.23.0: - resolution: {integrity: sha512-LdNBQfOe0dY2oJH2sAsrxazpgfFQo5yXGxe96QA8UWB5uu+433PrUbkv8gQ5RmrRCqUTPQ0aOrIyAdBr1aB03Q==} - engines: {node: '>=14.0'} - twoslash-protocol@0.2.12: resolution: {integrity: sha512-5qZLXVYfZ9ABdjqbvPc4RWMr7PrpPaaDSeaYY55vl/w1j6H6kzsWK/urAEIXlzYlyrFmyz1UbwIt+AA0ck+wbg==} @@ -19463,9 +19420,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - urlpattern-polyfill@10.0.0: resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} @@ -19989,10 +19943,6 @@ packages: xml@1.0.1: resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} - xmlbuilder@13.0.2: - resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==} - engines: {node: '>=6.0'} - xorshift@1.2.0: resolution: {integrity: sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g==} @@ -23541,7 +23491,7 @@ snapshots: '@graphql-mesh/hmac-upstream-signature': 2.0.8(graphql@16.12.0) '@graphql-mesh/plugin-response-cache': 0.104.18(graphql@16.12.0) '@graphql-mesh/transport-common': 1.0.12(graphql@16.12.0)(pino@10.3.0) - '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) + '@graphql-mesh/types': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-tools/batch-delegate': 10.0.8(graphql@16.12.0) '@graphql-tools/delegate': 12.0.2(graphql@16.12.0) @@ -23767,7 +23717,7 @@ snapshots: '@graphql-hive/logger': 1.0.9(pino@10.3.0) '@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0) '@graphql-mesh/transport-common': 1.0.12(graphql@16.12.0)(pino@10.3.0) - '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) + '@graphql-mesh/types': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@opentelemetry/api': 1.9.0 @@ -24248,7 +24198,7 @@ snapshots: '@graphql-hive/logger': 1.0.9(pino@10.3.0) '@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0) '@graphql-mesh/transport-common': 1.0.12(graphql@16.12.0)(pino@10.3.0) - '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) + '@graphql-mesh/types': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-tools/batch-execute': 10.0.4(graphql@16.12.0) '@graphql-tools/delegate': 12.0.2(graphql@16.12.0) @@ -24306,7 +24256,7 @@ snapshots: '@graphql-mesh/hmac-upstream-signature@2.0.8(graphql@16.12.0)': dependencies: '@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0) - '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) + '@graphql-mesh/types': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-tools/executor-common': 1.0.5(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) @@ -24433,7 +24383,7 @@ snapshots: '@envelop/response-cache': 9.0.0(@envelop/core@5.5.1)(graphql@16.12.0) '@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0) '@graphql-mesh/string-interpolation': 0.5.9(graphql@16.12.0) - '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) + '@graphql-mesh/types': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-tools/utils': 10.9.1(graphql@16.12.0) '@graphql-yoga/plugin-response-cache': 3.15.4(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0) @@ -24539,7 +24489,7 @@ snapshots: '@graphql-hive/logger': 1.0.9(pino@10.3.0) '@graphql-hive/pubsub': 2.1.1(ioredis@5.8.2) '@graphql-hive/signal': 2.0.0 - '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) + '@graphql-mesh/types': 0.104.16(graphql@16.12.0) '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/executor-common': 1.0.5(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) @@ -24638,6 +24588,21 @@ snapshots: - utf-8-validate - winston + '@graphql-mesh/types@0.104.16(graphql@16.12.0)': + dependencies: + '@graphql-hive/pubsub': 2.1.1(ioredis@5.8.2) + '@graphql-tools/batch-delegate': 10.0.5(graphql@16.12.0) + '@graphql-tools/delegate': 11.1.3(graphql@16.12.0) + '@graphql-tools/utils': 10.9.1(graphql@16.12.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/disposablestack': 0.0.6 + graphql: 16.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@nats-io/nats-core' + - ioredis + '@graphql-mesh/types@0.104.16(graphql@16.12.0)(ioredis@5.8.2)': dependencies: '@graphql-hive/pubsub': 2.1.1(ioredis@5.8.2) @@ -24673,7 +24638,7 @@ snapshots: '@envelop/instrumentation': 1.0.0 '@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0) '@graphql-mesh/string-interpolation': 0.5.9(graphql@16.12.0) - '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) + '@graphql-mesh/types': 0.104.16(graphql@16.12.0) '@graphql-tools/batch-delegate': 10.0.5(graphql@16.12.0) '@graphql-tools/delegate': 11.1.3(graphql@16.12.0) '@graphql-tools/utils': 10.9.1(graphql@16.12.0) @@ -30905,7 +30870,7 @@ snapshots: '@slack/types': 2.16.0 '@types/node': 24.10.12 '@types/retry': 0.12.0 - axios: 1.13.5(debug@4.4.1) + axios: 1.13.5 eventemitter3: 5.0.1 form-data: 4.0.4 is-electron: 2.2.2 @@ -33777,9 +33742,9 @@ snapshots: axe-core@4.7.0: {} - axios@1.13.5(debug@4.4.1): + axios@1.13.5: dependencies: - follow-redirects: 1.15.11(debug@4.4.1) + follow-redirects: 1.15.11 form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -34741,12 +34706,6 @@ snapshots: crelt@1.0.6: {} - cross-fetch@3.1.8(encoding@0.1.13): - dependencies: - node-fetch: 2.6.12(encoding@0.1.13) - transitivePeerDependencies: - - encoding - cross-inspect@1.0.1: dependencies: tslib: 2.8.1 @@ -36405,9 +36364,7 @@ snapshots: fn-name@3.0.0: {} - follow-redirects@1.15.11(debug@4.4.1): - optionalDependencies: - debug: 4.4.1(supports-color@8.1.1) + follow-redirects@1.15.11: {} for-each@0.3.3: dependencies: @@ -37682,8 +37639,6 @@ snapshots: indent-string@5.0.0: {} - inflation@2.1.0: {} - inflight@1.0.6: dependencies: once: 1.4.0 @@ -38450,8 +38405,6 @@ snapshots: libbase64: 0.1.0 libqp: 1.1.0 - libphonenumber-js@1.12.17: {} - libqp@1.1.0: {} lie@3.1.1: @@ -40927,10 +40880,6 @@ snapshots: pirates@4.0.5: {} - pkce-challenge@3.1.0: - dependencies: - crypto-js: 4.2.0 - pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -41244,8 +41193,6 @@ snapshots: pseudomap@1.0.2: {} - psl@1.8.0: {} - pump@3.0.0: dependencies: end-of-stream: 1.4.4 @@ -41275,8 +41222,6 @@ snapshots: filter-obj: 5.1.0 split-on-first: 3.0.0 - querystringify@2.2.0: {} - queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -41902,8 +41847,6 @@ snapshots: transitivePeerDependencies: - supports-color - requires-port@1.0.0: {} - reselect@5.1.1: {} resolve-alpn@1.2.1: {} @@ -42175,8 +42118,6 @@ snapshots: scheduler@0.27.0: {} - scmp@2.1.0: {} - scroll-into-view-if-needed@3.1.0: dependencies: compute-scroll-into-view: 3.1.1 @@ -42879,24 +42820,6 @@ snapshots: supertokens-js-override@0.0.4: {} - supertokens-node@16.7.5(encoding@0.1.13): - dependencies: - content-type: 1.0.5 - cookie: 0.7.2 - cross-fetch: 3.1.8(encoding@0.1.13) - debug: 4.4.1(supports-color@8.1.1) - inflation: 2.1.0 - jose: 4.15.9 - libphonenumber-js: 1.12.17 - nodemailer: 7.0.11 - pkce-challenge: 3.1.0 - psl: 1.8.0 - supertokens-js-override: 0.0.4 - twilio: 4.23.0(debug@4.4.1) - transitivePeerDependencies: - - encoding - - supports-color - supertokens-web-js@0.9.0: dependencies: supertokens-js-override: 0.0.4 @@ -43430,20 +43353,6 @@ snapshots: tweetnacl@0.14.5: {} - twilio@4.23.0(debug@4.4.1): - dependencies: - axios: 1.13.5(debug@4.4.1) - dayjs: 1.11.13 - https-proxy-agent: 5.0.1 - jsonwebtoken: 9.0.3 - qs: 6.14.2 - scmp: 2.1.0 - url-parse: 1.5.10 - xmlbuilder: 13.0.2 - transitivePeerDependencies: - - debug - - supports-color - twoslash-protocol@0.2.12: {} twoslash@0.2.12(typescript@5.9.3): @@ -43771,11 +43680,6 @@ snapshots: dependencies: punycode: 2.1.1 - url-parse@1.5.10: - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - urlpattern-polyfill@10.0.0: {} urlpattern-polyfill@8.0.2: {} @@ -44188,7 +44092,7 @@ snapshots: wait-on@9.0.3: dependencies: - axios: 1.13.5(debug@4.4.1) + axios: 1.13.5 joi: 18.0.2 lodash: 4.17.23 minimist: 1.2.8 @@ -44401,8 +44305,6 @@ snapshots: xml@1.0.1: {} - xmlbuilder@13.0.2: {} - xorshift@1.2.0: {} xtend@4.0.2: {} diff --git a/scripts/seed-insights.mts b/scripts/seed-insights.mts index 1842a2abf..ab7aea942 100644 --- a/scripts/seed-insights.mts +++ b/scripts/seed-insights.mts @@ -13,7 +13,7 @@ */ import * as readline from 'node:readline/promises'; -import { createTRPCProxyClient, httpLink } from '@trpc/client'; +import setCookie from 'set-cookie-parser'; import type { CollectedOperation } from '../integration-tests/testkit/usage'; process.env.RUN_AGAINST_LOCAL_SERVICES = '1'; @@ -42,92 +42,69 @@ const password = 'ilikebigturtlesandicannotlie47'; async function signInOrSignUp( email: string, ): Promise<{ access_token: string; refresh_token: string }> { - const supertokensUri = ensureEnv('SUPERTOKENS_CONNECTION_URI'); - const apiKey = ensureEnv('SUPERTOKENS_API_KEY'); - const headers = { - 'content-type': 'application/json; charset=UTF-8', - 'api-key': apiKey, - 'cdi-version': '4.0', - }; - const body = JSON.stringify({ email, password }); - - // Try signup first - let res = await fetch(`${supertokensUri}/appid-public/public/recipe/signup`, { - method: 'POST', - headers, - body, - }); - let data = (await res.json()) as { status: string; user?: { id: string; emails: string[] } }; - - // If user already exists, look them up by email (avoids needing their password) - if (data.status === 'EMAIL_ALREADY_EXISTS_ERROR') { - res = await fetch( - `${supertokensUri}/appid-public/public/recipe/user?email=${encodeURIComponent(email)}`, - { headers }, - ); - const lookupData = (await res.json()) as { - status: string; - user?: { id: string; emails: string[] }; - }; - if (lookupData.status !== 'OK' || !lookupData.user) { - throw new Error(`User lookup failed for ${email}: ${JSON.stringify(lookupData)}`); - } - data = { status: 'OK', user: lookupData.user }; - } - - if (data.status !== 'OK' || !data.user) { - throw new Error(`Auth failed for ${email}: ${JSON.stringify(data)}`); - } - - const superTokensUserId = data.user.id; - - // Ensure user exists in Hive DB const graphqlAddress = await getServiceHost('server', 8082); - const internalApi = createTRPCProxyClient({ - links: [httpLink({ url: `http://${graphqlAddress}/trpc`, fetch })], - }); - const ensureUserResult = await internalApi.ensureUser.mutate({ - superTokensUserId, - email, - oidcIntegrationId: null, - firstName: null, - lastName: null, - }); - if (!ensureUserResult.ok) { - throw new Error(`ensureUser failed: ${ensureUserResult.reason}`); - } - // Create session - const sessionPayload = { - version: '2', - superTokensUserId, - userId: ensureUserResult.user.id, - oidcIntegrationId: null, - email, - }; - const sessionRes = await fetch(`${supertokensUri}/appid-public/public/recipe/session`, { + let response = await fetch(`http://${graphqlAddress}/auth-api/signup`, { method: 'POST', - headers: { ...headers, rid: 'session' }, body: JSON.stringify({ - enableAntiCsrf: false, - userId: superTokensUserId, - userDataInDatabase: sessionPayload, - userDataInJWT: sessionPayload, + formFields: [ + { + id: 'email', + value: email, + }, + { + id: 'password', + value: password, + }, + ], }), + headers: { + 'content-type': 'application/json', + }, }); - const sessionData = (await sessionRes.json()) as { - accessToken?: { token: string }; - refreshToken?: { token: string }; - }; - if (!sessionData.accessToken?.token || !sessionData.refreshToken?.token) { - throw new Error(`Session creation failed: ${JSON.stringify(sessionData)}`); + let body = await response.json(); + if (body.status === 'OK') { + const cookies = setCookie.parse(response.headers.getSetCookie()); + return { + access_token: cookies.find(c => c.name === 'sAccessToken')?.value ?? '', + refresh_token: cookies.find(c => c.name === 'sRefreshToken')?.value ?? '', + }; } - return { - access_token: sessionData.accessToken.token, - refresh_token: sessionData.refreshToken.token, - }; + console.log('signup response', JSON.stringify(body, null, 2)); + console.log('attempt sign in'); + + response = await fetch(`http://${graphqlAddress}/auth-api/signin`, { + method: 'POST', + body: JSON.stringify({ + formFields: [ + { + id: 'email', + value: email, + }, + { + id: 'password', + value: password, + }, + ], + }), + headers: { + 'content-type': 'application/json', + }, + }); + + body = await response.json(); + + if (body.status === 'OK') { + const cookies = setCookie.parse(response.headers.getSetCookie()); + return { + access_token: cookies.find(c => c.name === 'sAccessToken')?.value ?? '', + refresh_token: cookies.find(c => c.name === 'sRefreshToken')?.value ?? '', + }; + } + + throw new Error('Failed to sign in or up ' + JSON.stringify(body, null, 2)); } // ---------------------------------------------------------------------------