mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
fix: inherit OIDC provider email (#7810)
This commit is contained in:
parent
e3afb155b2
commit
7aac422acc
15 changed files with 695 additions and 69 deletions
5
.changeset/honest-knives-sleep.md
Normal file
5
.changeset/honest-knives-sleep.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'hive': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Propagate updated email address from OIDC provider. This fixes a bug where a user was locked out of the Hive account after the email of the user on the OIDC provider side changed.
|
||||||
7
.github/workflows/tests-integration.yaml
vendored
7
.github/workflows/tests-integration.yaml
vendored
|
|
@ -54,6 +54,13 @@ jobs:
|
||||||
uses: mikefarah/yq@4839dbbf80445070a31c7a9c1055da527db2d5ee # v4.44.6
|
uses: mikefarah/yq@4839dbbf80445070a31c7a9c1055da527db2d5ee # v4.44.6
|
||||||
with:
|
with:
|
||||||
cmd: yq -i 'del(.services.*.volumes)' docker/docker-compose.community.yml
|
cmd: yq -i 'del(.services.*.volumes)' docker/docker-compose.community.yml
|
||||||
|
# tests need to access host machine for OIDC stuff
|
||||||
|
- name: make host machine accessible to server container
|
||||||
|
uses: mikefarah/yq@4839dbbf80445070a31c7a9c1055da527db2d5ee # v4.44.6
|
||||||
|
with:
|
||||||
|
cmd:
|
||||||
|
yq -i '.services.server.extra_hosts[] |= sub("host-gateway"; "172.17.0.1")'
|
||||||
|
./integration-tests/docker-compose.integration.yaml
|
||||||
|
|
||||||
- name: get cpu count for vitest
|
- name: get cpu count for vitest
|
||||||
id: cpu-cores
|
id: cpu-cores
|
||||||
|
|
|
||||||
|
|
@ -61,40 +61,8 @@ export default defineConfig({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async getEmailConfirmationLink(input: string | { email: string; now: number }) {
|
async getEmailConfirmationLink(input: string | { email: string; now: number }) {
|
||||||
const email = typeof input === 'string' ? input : input.email;
|
const url = await seed.pollForEmailVerificationLink(input);
|
||||||
const now = new Date(
|
return url.pathname + url.search;
|
||||||
typeof input === 'string' ? Date.now() - 10_000 : input.now,
|
|
||||||
).toISOString();
|
|
||||||
const url = new URL('http://localhost:3014/_history');
|
|
||||||
url.searchParams.set('after', now);
|
|
||||||
|
|
||||||
return await asyncRetry(
|
|
||||||
async () => {
|
|
||||||
const emails = await fetch(url.toString())
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(emails =>
|
|
||||||
emails.filter(e => e.to === email && e.subject === 'Verify your email'),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (emails.length === 0) {
|
|
||||||
throw new Error('Could not find email');
|
|
||||||
}
|
|
||||||
|
|
||||||
// take the latest one
|
|
||||||
const result = emails[emails.length - 1];
|
|
||||||
|
|
||||||
const urlMatch = result.body.match(/href=\"(http:\/\/[^\s"]+)/);
|
|
||||||
if (!urlMatch) throw new Error('No URL found in email');
|
|
||||||
|
|
||||||
const confirmUrl = new URL(urlMatch[1]);
|
|
||||||
return confirmUrl.pathname + confirmUrl.search;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
retries: 10,
|
|
||||||
minTimeout: 1000,
|
|
||||||
maxTimeout: 10000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,12 @@ services:
|
||||||
# Auth
|
# Auth
|
||||||
WEB_APP_URL: '${HIVE_APP_BASE_URL}'
|
WEB_APP_URL: '${HIVE_APP_BASE_URL}'
|
||||||
AUTH_ORGANIZATION_OIDC: '1'
|
AUTH_ORGANIZATION_OIDC: '1'
|
||||||
AUTH_REQUIRE_EMAIL_VERIFICATION: '0'
|
AUTH_REQUIRE_EMAIL_VERIFICATION: '1'
|
||||||
SUPERTOKENS_CONNECTION_URI: http://supertokens:3567
|
SUPERTOKENS_CONNECTION_URI: http://supertokens:3567
|
||||||
SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}'
|
SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}'
|
||||||
GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082
|
GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082
|
||||||
|
extra_hosts:
|
||||||
|
- 'host.docker.internal:host-gateway'
|
||||||
|
|
||||||
broker:
|
broker:
|
||||||
image: redpandadata/redpanda:latest
|
image: redpandadata/redpanda:latest
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,14 @@
|
||||||
"@hive/commerce": "workspace:*",
|
"@hive/commerce": "workspace:*",
|
||||||
"@hive/schema": "workspace:*",
|
"@hive/schema": "workspace:*",
|
||||||
"@hive/server": "workspace:*",
|
"@hive/server": "workspace:*",
|
||||||
|
"@hive/service-common": "workspace:*",
|
||||||
"@hive/storage": "workspace:*",
|
"@hive/storage": "workspace:*",
|
||||||
"@theguild/federation-composition": "0.21.3",
|
"@theguild/federation-composition": "0.21.3",
|
||||||
"@trpc/client": "10.45.3",
|
"@trpc/client": "10.45.3",
|
||||||
"@trpc/server": "10.45.3",
|
"@trpc/server": "10.45.3",
|
||||||
"@types/async-retry": "1.4.8",
|
"@types/async-retry": "1.4.8",
|
||||||
"@types/dockerode": "3.3.43",
|
"@types/dockerode": "3.3.43",
|
||||||
|
"@types/set-cookie-parser": "2.4.10",
|
||||||
"async-retry": "1.3.3",
|
"async-retry": "1.3.3",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"csv-parse": "5.6.0",
|
"csv-parse": "5.6.0",
|
||||||
|
|
@ -37,6 +39,7 @@
|
||||||
"graphql-sse": "2.6.0",
|
"graphql-sse": "2.6.0",
|
||||||
"human-id": "4.1.1",
|
"human-id": "4.1.1",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.8.2",
|
||||||
|
"set-cookie-parser": "2.7.1",
|
||||||
"slonik": "30.4.4",
|
"slonik": "30.4.4",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
|
|
|
||||||
368
integration-tests/testkit/oidc-integration.ts
Normal file
368
integration-tests/testkit/oidc-integration.ts
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
import type { AddressInfo } from 'node:net';
|
||||||
|
import humanId from 'human-id';
|
||||||
|
import setCookie from 'set-cookie-parser';
|
||||||
|
import { sql, type DatabasePool } from 'slonik';
|
||||||
|
import z from 'zod';
|
||||||
|
import formDataPlugin from '@fastify/formbody';
|
||||||
|
import { createServer, type FastifyReply, type FastifyRequest } from '@hive/service-common';
|
||||||
|
import { graphql } from './gql';
|
||||||
|
import { execute } from './graphql';
|
||||||
|
import { getServiceHost, pollForEmailVerificationLink } from './utils';
|
||||||
|
|
||||||
|
const apiAddress = await getServiceHost('server', 8082);
|
||||||
|
|
||||||
|
async function createMockOIDCServer() {
|
||||||
|
const host =
|
||||||
|
process.env.RUN_AGAINST_LOCAL_SERVICES === '1' ? 'localhost' : 'host.docker.internal';
|
||||||
|
const server = await createServer({
|
||||||
|
sentryErrorHandler: false,
|
||||||
|
log: {
|
||||||
|
requests: false,
|
||||||
|
level: 'silent',
|
||||||
|
},
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
await server.register(formDataPlugin);
|
||||||
|
|
||||||
|
let registeredHandler: typeof handler;
|
||||||
|
|
||||||
|
async function handler(request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||||
|
if (!handler) {
|
||||||
|
throw new Error('No handler registered');
|
||||||
|
}
|
||||||
|
return await registeredHandler(request, reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/token',
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/userinfo',
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.listen({
|
||||||
|
port: 0,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: 'http://' + host + ':' + (server.server.address() as AddressInfo).port,
|
||||||
|
setHandler(newHandler: typeof handler) {
|
||||||
|
registeredHandler = newHandler;
|
||||||
|
},
|
||||||
|
[Symbol.asyncDispose]: () => {
|
||||||
|
server.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateOIDCIntegrationMutation = graphql(`
|
||||||
|
mutation TestKit_OIDCIntegration_CreateOIDCIntegrationMutation(
|
||||||
|
$input: CreateOIDCIntegrationInput!
|
||||||
|
) {
|
||||||
|
createOIDCIntegration(input: $input) {
|
||||||
|
ok {
|
||||||
|
createdOIDCIntegration {
|
||||||
|
id
|
||||||
|
clientId
|
||||||
|
clientSecretPreview
|
||||||
|
tokenEndpoint
|
||||||
|
userinfoEndpoint
|
||||||
|
authorizationEndpoint
|
||||||
|
additionalScopes
|
||||||
|
oidcUserJoinOnly
|
||||||
|
oidcUserAccessOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error {
|
||||||
|
message
|
||||||
|
details {
|
||||||
|
clientId
|
||||||
|
clientSecret
|
||||||
|
tokenEndpoint
|
||||||
|
userinfoEndpoint
|
||||||
|
authorizationEndpoint
|
||||||
|
additionalScopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const UpdateOIDCIntegrationMutation = graphql(`
|
||||||
|
mutation TestKit_OIDCIntegration_UpdateOIDCIntegrationMutation(
|
||||||
|
$input: UpdateOIDCIntegrationInput!
|
||||||
|
) {
|
||||||
|
updateOIDCIntegration(input: $input) {
|
||||||
|
ok {
|
||||||
|
updatedOIDCIntegration {
|
||||||
|
id
|
||||||
|
tokenEndpoint
|
||||||
|
userinfoEndpoint
|
||||||
|
authorizationEndpoint
|
||||||
|
clientId
|
||||||
|
clientSecretPreview
|
||||||
|
additionalScopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error {
|
||||||
|
message
|
||||||
|
details {
|
||||||
|
clientId
|
||||||
|
clientSecret
|
||||||
|
tokenEndpoint
|
||||||
|
userinfoEndpoint
|
||||||
|
authorizationEndpoint
|
||||||
|
additionalScopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const SendVerificationEmailMutation = graphql(`
|
||||||
|
mutation TestKit_OIDCIntegration_SendVerificationEmailMutation(
|
||||||
|
$input: SendVerificationEmailInput!
|
||||||
|
) {
|
||||||
|
sendVerificationEmail(input: $input) {
|
||||||
|
ok {
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
error {
|
||||||
|
message
|
||||||
|
emailAlreadyVerified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const VerifyEmailMutation = graphql(`
|
||||||
|
mutation TestKit_OIDCIntegration_VerifyEmailMutation($input: VerifyEmailInput!) {
|
||||||
|
verifyEmail(input: $input) {
|
||||||
|
ok {
|
||||||
|
verified
|
||||||
|
}
|
||||||
|
error {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export async function createOIDCIntegration(args: {
|
||||||
|
organizationId: string;
|
||||||
|
accessToken: string;
|
||||||
|
getPool: () => Promise<DatabasePool>;
|
||||||
|
}) {
|
||||||
|
const { accessToken: authToken, getPool } = args;
|
||||||
|
const result = await execute({
|
||||||
|
document: CreateOIDCIntegrationMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: args.organizationId,
|
||||||
|
additionalScopes: [],
|
||||||
|
authorizationEndpoint: 'http://localhost:6666/noop/authoriation',
|
||||||
|
tokenEndpoint: 'http://localhost:6666/noop/token',
|
||||||
|
userinfoEndpoint: 'http://localhost:666/noop/userinfo',
|
||||||
|
clientId: 'noop',
|
||||||
|
clientSecret: 'noop',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authToken,
|
||||||
|
}).then(r => r.expectNoGraphQLErrors());
|
||||||
|
|
||||||
|
if (!result.createOIDCIntegration.ok) {
|
||||||
|
throw new Error(result.createOIDCIntegration.error?.message ?? 'Unexpected error.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oidcIntegration = result.createOIDCIntegration.ok.createdOIDCIntegration;
|
||||||
|
|
||||||
|
return {
|
||||||
|
oidcIntegration,
|
||||||
|
async registerFakeDomain() {
|
||||||
|
const randomDomain =
|
||||||
|
humanId({
|
||||||
|
separator: '',
|
||||||
|
capitalize: false,
|
||||||
|
}) + '.local';
|
||||||
|
|
||||||
|
const pool = await getPool();
|
||||||
|
const query = sql`
|
||||||
|
INSERT INTO "oidc_integration_domains" (
|
||||||
|
"organization_id"
|
||||||
|
, "oidc_integration_id"
|
||||||
|
, "domain_name"
|
||||||
|
, "verified_at"
|
||||||
|
) VALUES (
|
||||||
|
${args.organizationId}
|
||||||
|
, ${oidcIntegration.id}
|
||||||
|
, ${randomDomain}
|
||||||
|
, NOW()
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
await pool.query(query);
|
||||||
|
return randomDomain;
|
||||||
|
},
|
||||||
|
async createMockServerAndUpdateIntegrationEndpoints(args?: {
|
||||||
|
additionalScopes?: Array<string>;
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
}) {
|
||||||
|
const server = await createMockOIDCServer();
|
||||||
|
|
||||||
|
const result = await execute({
|
||||||
|
document: UpdateOIDCIntegrationMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
oidcIntegrationId: oidcIntegration.id,
|
||||||
|
authorizationEndpoint: server.url + '/authorize',
|
||||||
|
tokenEndpoint: server.url + '/token',
|
||||||
|
userinfoEndpoint: server.url + '/userinfo',
|
||||||
|
additionalScopes: args?.additionalScopes,
|
||||||
|
clientId: args?.clientId,
|
||||||
|
clientSecret: args?.clientSecret,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authToken,
|
||||||
|
}).then(r => r.expectNoGraphQLErrors());
|
||||||
|
|
||||||
|
if (!result.updateOIDCIntegration.ok) {
|
||||||
|
throw new Error(result.updateOIDCIntegration.error?.message ?? 'Unexpected error.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
setHandler: server.setHandler,
|
||||||
|
setUser(args: { email: string; sub: string }) {
|
||||||
|
server.setHandler(async (req, res) => {
|
||||||
|
if (req.routeOptions.url === '/token') {
|
||||||
|
return res.status(200).send({
|
||||||
|
access_token: 'yolo',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.routeOptions.url === '/userinfo') {
|
||||||
|
return res.status(200).send({
|
||||||
|
sub: args.sub,
|
||||||
|
email: args.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('unhandled', req.routeOptions.url);
|
||||||
|
return res.status(404).send();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async runGetAuthorizationUrl() {
|
||||||
|
const baseUrl = 'http://' + apiAddress;
|
||||||
|
const url = new URL('http://' + apiAddress + '/auth-api/authorisationurl');
|
||||||
|
url.searchParams.set('thirdPartyId', 'oidc');
|
||||||
|
url.searchParams.set('redirectURIOnProviderDashboard', baseUrl + '/');
|
||||||
|
url.searchParams.set('oidc_id', oidcIntegration.id);
|
||||||
|
const result = await fetch(url).then(res => res.json());
|
||||||
|
|
||||||
|
const urlWithQueryParams = new URL(result.urlWithQueryParams);
|
||||||
|
return {
|
||||||
|
codeChallenge: urlWithQueryParams.searchParams.get('code_challenge') ?? '',
|
||||||
|
state: urlWithQueryParams.searchParams.get('state') ?? '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async runSignInUp(args: { state: string; code?: string }) {
|
||||||
|
const url = new URL('http://' + apiAddress + '/auth-api/signinup');
|
||||||
|
url.searchParams.set('oidc_id', oidcIntegration.id);
|
||||||
|
|
||||||
|
const result = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
thirdPartyId: 'oidc',
|
||||||
|
redirectURIInfo: {
|
||||||
|
redirectURIOnProviderDashboard: '/',
|
||||||
|
redirectURIQueryParams: {
|
||||||
|
state: args.state,
|
||||||
|
code: args.code ?? 'noop',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'st-auth-mode': 'cookie',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 200) {
|
||||||
|
throw new Error('Failed ' + result.status + (await result.text()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawBody = await result.json();
|
||||||
|
|
||||||
|
const body = z
|
||||||
|
.object({
|
||||||
|
user: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
emails: z.array(z.string()),
|
||||||
|
loginMethods: z.array(
|
||||||
|
z.object({
|
||||||
|
recipeUserId: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.parse(rawBody);
|
||||||
|
const cookies = setCookie.parse(result.headers.getSetCookie());
|
||||||
|
return {
|
||||||
|
accessToken: cookies.find(c => c.name === 'sAccessToken')?.value ?? ('' as string),
|
||||||
|
user: {
|
||||||
|
id: body.user.id,
|
||||||
|
email: body.user.emails[0],
|
||||||
|
userIdentityId: body.user.loginMethods[0]?.recipeUserId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async confirmEmail(args: { userIdentityId: string; email: string }) {
|
||||||
|
const now = Date.now();
|
||||||
|
const sendMail = await execute({
|
||||||
|
document: SendVerificationEmailMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
userIdentityId: args.userIdentityId,
|
||||||
|
resend: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authToken,
|
||||||
|
}).then(e => e.expectNoGraphQLErrors());
|
||||||
|
|
||||||
|
if (!sendMail.sendVerificationEmail.ok) {
|
||||||
|
throw new Error(sendMail.sendVerificationEmail.error?.message ?? 'Unknown error.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await pollForEmailVerificationLink({
|
||||||
|
email: args.email,
|
||||||
|
now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = url.searchParams.get('token') ?? '';
|
||||||
|
|
||||||
|
const confirmMail = await execute({
|
||||||
|
document: VerifyEmailMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
userIdentityId: args.userIdentityId,
|
||||||
|
email: args.email,
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authToken,
|
||||||
|
}).then(e => e.expectNoGraphQLErrors());
|
||||||
|
|
||||||
|
if (!confirmMail.verifyEmail.ok) {
|
||||||
|
throw new Error(confirmMail.verifyEmail.error?.message ?? 'Unknown error.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -53,13 +53,9 @@ import {
|
||||||
updateTargetValidationSettings,
|
updateTargetValidationSettings,
|
||||||
} from './flow';
|
} from './flow';
|
||||||
import * as GraphQLSchema from './gql/graphql';
|
import * as GraphQLSchema from './gql/graphql';
|
||||||
import {
|
import { ProjectType, SchemaPolicyInput, TargetAccessScope } from './gql/graphql';
|
||||||
BreakingChangeFormulaType,
|
|
||||||
ProjectType,
|
|
||||||
SchemaPolicyInput,
|
|
||||||
TargetAccessScope,
|
|
||||||
} from './gql/graphql';
|
|
||||||
import { execute } from './graphql';
|
import { execute } from './graphql';
|
||||||
|
import { createOIDCIntegration } from './oidc-integration.js';
|
||||||
import {
|
import {
|
||||||
CreateSavedFilterMutation,
|
CreateSavedFilterMutation,
|
||||||
DeleteSavedFilterMutation,
|
DeleteSavedFilterMutation,
|
||||||
|
|
@ -70,7 +66,7 @@ import {
|
||||||
} from './saved-filters';
|
} from './saved-filters';
|
||||||
import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from './schema-policy';
|
import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from './schema-policy';
|
||||||
import { collect, CollectedOperation, legacyCollect } from './usage';
|
import { collect, CollectedOperation, legacyCollect } from './usage';
|
||||||
import { generateUnique, getServiceHost } from './utils';
|
import { generateUnique, getServiceHost, pollForEmailVerificationLink } from './utils';
|
||||||
|
|
||||||
function createConnectionPool() {
|
function createConnectionPool() {
|
||||||
const pg = {
|
const pg = {
|
||||||
|
|
@ -106,11 +102,28 @@ export function initSeed() {
|
||||||
return sharedDBPoolPromise.then(res => res.pool);
|
return sharedDBPoolPromise.then(res => res.pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doAuthenticate(email: string, oidcIntegrationId?: string) {
|
async function doAuthenticate(
|
||||||
return await authenticate(await getPool(), email, oidcIntegrationId);
|
email: string,
|
||||||
|
opts?: {
|
||||||
|
oidcIntegrationId?: string;
|
||||||
|
verifyEmail?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const auth = await authenticate(await getPool(), email, opts?.oidcIntegrationId);
|
||||||
|
|
||||||
|
if (opts?.verifyEmail ?? true) {
|
||||||
|
const pool = await getPool();
|
||||||
|
await pool.query(sql`
|
||||||
|
INSERT INTO "email_verifications" ("user_identity_id", "email", "verified_at")
|
||||||
|
VALUES (${auth.supertokensUserId}, ${email}, NOW())
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
pollForEmailVerificationLink,
|
||||||
async purgeOIDCDomains() {
|
async purgeOIDCDomains() {
|
||||||
const pool = await getPool();
|
const pool = await getPool();
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
|
|
@ -162,15 +175,9 @@ export function initSeed() {
|
||||||
},
|
},
|
||||||
async createOwner(verifyEmail: boolean = true) {
|
async createOwner(verifyEmail: boolean = true) {
|
||||||
const ownerEmail = userEmail(generateUnique());
|
const ownerEmail = userEmail(generateUnique());
|
||||||
const auth = await doAuthenticate(ownerEmail);
|
const auth = await doAuthenticate(ownerEmail, {
|
||||||
|
verifyEmail,
|
||||||
if (verifyEmail) {
|
});
|
||||||
const pool = await getPool();
|
|
||||||
await pool.query(sql`
|
|
||||||
INSERT INTO "email_verifications" ("user_identity_id", "email", "verified_at")
|
|
||||||
VALUES (${auth.supertokensUserId}, ${ownerEmail}, NOW())
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ownerRefreshToken = auth.refresh_token;
|
const ownerRefreshToken = auth.refresh_token;
|
||||||
const ownerToken = auth.access_token;
|
const ownerToken = auth.access_token;
|
||||||
|
|
@ -1159,9 +1166,10 @@ export function initSeed() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const memberEmail = userEmail(generateUnique());
|
const memberEmail = userEmail(generateUnique());
|
||||||
const memberToken = await doAuthenticate(memberEmail, oidcIntegrationId).then(
|
const memberToken = await doAuthenticate(memberEmail, {
|
||||||
r => r.access_token,
|
oidcIntegrationId,
|
||||||
);
|
verifyEmail: true,
|
||||||
|
}).then(r => r.access_token);
|
||||||
|
|
||||||
if (!oidcIntegrationId) {
|
if (!oidcIntegrationId) {
|
||||||
const invitationResult = await inviteToOrganization(
|
const invitationResult = await inviteToOrganization(
|
||||||
|
|
@ -1375,6 +1383,13 @@ export function initSeed() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
createOIDCIntegration() {
|
||||||
|
return createOIDCIntegration({
|
||||||
|
organizationId: organization.id,
|
||||||
|
accessToken: ownerToken,
|
||||||
|
getPool: getPool,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncRetry from 'async-retry';
|
||||||
import Docker from 'dockerode';
|
import Docker from 'dockerode';
|
||||||
import { humanId } from 'human-id';
|
import { humanId } from 'human-id';
|
||||||
|
|
||||||
|
|
@ -122,3 +123,37 @@ export function assertNonNullish<T>(
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function pollForEmailVerificationLink(input: string | { email: string; now: number }) {
|
||||||
|
const email = typeof input === 'string' ? input : input.email;
|
||||||
|
const now = new Date(typeof input === 'string' ? Date.now() - 10_000 : input.now).toISOString();
|
||||||
|
const url = new URL('http://localhost:3014/_history');
|
||||||
|
url.searchParams.set('after', now);
|
||||||
|
|
||||||
|
return await asyncRetry(
|
||||||
|
async () => {
|
||||||
|
const emails = await fetch(url.toString())
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(emails =>
|
||||||
|
emails.filter((e: any) => e.to === email && e.subject === 'Verify your email'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emails.length === 0) {
|
||||||
|
throw new Error('Could not find email');
|
||||||
|
}
|
||||||
|
|
||||||
|
// take the latest one
|
||||||
|
const result = emails[emails.length - 1];
|
||||||
|
|
||||||
|
const urlMatch = result.body.match(/href=\"(http:\/\/[^\s"]+)/);
|
||||||
|
if (!urlMatch) throw new Error('No URL found in email');
|
||||||
|
|
||||||
|
return new URL(urlMatch[1]);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retries: 10,
|
||||||
|
minTimeout: 1000,
|
||||||
|
maxTimeout: 10000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
157
integration-tests/tests/api/auth/oidc.spec.ts
Normal file
157
integration-tests/tests/api/auth/oidc.spec.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
import { graphql } from 'testkit/gql';
|
||||||
|
import { execute } from 'testkit/graphql';
|
||||||
|
import { initSeed } from 'testkit/seed';
|
||||||
|
|
||||||
|
const TestMeQuery = graphql(`
|
||||||
|
query OIDC_TestMeQuery {
|
||||||
|
me {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
test.concurrent(
|
||||||
|
'User can sign in/up with OIDC provider and confirm their email',
|
||||||
|
async ({ expect }) => {
|
||||||
|
const seed = initSeed();
|
||||||
|
const email = seed.generateEmail();
|
||||||
|
const { createOrg } = await seed.createOwner();
|
||||||
|
const { createOIDCIntegration } = await createOrg();
|
||||||
|
|
||||||
|
const { createMockServerAndUpdateIntegrationEndpoints } = await createOIDCIntegration();
|
||||||
|
const oidc = await createMockServerAndUpdateIntegrationEndpoints();
|
||||||
|
|
||||||
|
const auth = await oidc.runGetAuthorizationUrl();
|
||||||
|
|
||||||
|
oidc.setUser({
|
||||||
|
sub: 'test-user',
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await oidc.runSignInUp({
|
||||||
|
state: auth.state,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error] = await execute({
|
||||||
|
document: TestMeQuery,
|
||||||
|
authToken: result.accessToken,
|
||||||
|
}).then(r => r.expectGraphQLErrors());
|
||||||
|
|
||||||
|
expect(error).toMatchObject({
|
||||||
|
extensions: {
|
||||||
|
code: 'VERIFY_EMAIL',
|
||||||
|
},
|
||||||
|
message: 'Your account is not verified. Please verify your email address.',
|
||||||
|
});
|
||||||
|
|
||||||
|
await oidc.confirmEmail(result.user);
|
||||||
|
const meResult = await execute({
|
||||||
|
document: TestMeQuery,
|
||||||
|
authToken: result.accessToken,
|
||||||
|
}).then(r => r.expectNoGraphQLErrors());
|
||||||
|
expect(meResult).toMatchObject({
|
||||||
|
me: {
|
||||||
|
email,
|
||||||
|
id: expect.any(String),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.concurrent(
|
||||||
|
'If the OIDC provider users email changes, the users email is updated upon login',
|
||||||
|
async ({ expect }) => {
|
||||||
|
const seed = initSeed();
|
||||||
|
const oldEmail = seed.generateEmail();
|
||||||
|
const newEmail = seed.generateEmail();
|
||||||
|
const { createOrg } = await seed.createOwner();
|
||||||
|
const { createOIDCIntegration } = await createOrg();
|
||||||
|
|
||||||
|
const { createMockServerAndUpdateIntegrationEndpoints } = await createOIDCIntegration();
|
||||||
|
const oidc = await createMockServerAndUpdateIntegrationEndpoints();
|
||||||
|
|
||||||
|
let auth = await oidc.runGetAuthorizationUrl();
|
||||||
|
|
||||||
|
oidc.setUser({
|
||||||
|
sub: 'test-user',
|
||||||
|
email: oldEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = await oidc.runSignInUp({
|
||||||
|
state: auth.state,
|
||||||
|
});
|
||||||
|
|
||||||
|
await oidc.confirmEmail(result.user);
|
||||||
|
|
||||||
|
let meResult = await execute({
|
||||||
|
document: TestMeQuery,
|
||||||
|
authToken: result.accessToken,
|
||||||
|
}).then(r => r.expectNoGraphQLErrors());
|
||||||
|
expect(meResult).toMatchObject({
|
||||||
|
me: {
|
||||||
|
email: oldEmail,
|
||||||
|
id: expect.any(String),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
auth = await oidc.runGetAuthorizationUrl();
|
||||||
|
|
||||||
|
oidc.setUser({
|
||||||
|
sub: 'test-user',
|
||||||
|
email: newEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
result = await oidc.runSignInUp({
|
||||||
|
state: auth.state,
|
||||||
|
});
|
||||||
|
await oidc.confirmEmail(result.user);
|
||||||
|
meResult = await execute({
|
||||||
|
document: TestMeQuery,
|
||||||
|
authToken: result.accessToken,
|
||||||
|
}).then(r => r.expectNoGraphQLErrors());
|
||||||
|
expect(meResult).toMatchObject({
|
||||||
|
me: {
|
||||||
|
email: newEmail,
|
||||||
|
id: expect.any(String),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.concurrent(
|
||||||
|
'User does not need to confirm their email if the domain is verified with the origanization',
|
||||||
|
async ({ expect }) => {
|
||||||
|
const seed = initSeed();
|
||||||
|
const { createOrg } = await seed.createOwner();
|
||||||
|
const { createOIDCIntegration } = await createOrg();
|
||||||
|
|
||||||
|
const { createMockServerAndUpdateIntegrationEndpoints, registerFakeDomain: registerDomain } =
|
||||||
|
await createOIDCIntegration();
|
||||||
|
const domain = await registerDomain();
|
||||||
|
const oidc = await createMockServerAndUpdateIntegrationEndpoints();
|
||||||
|
|
||||||
|
const email = 'foo@' + domain;
|
||||||
|
|
||||||
|
let auth = await oidc.runGetAuthorizationUrl();
|
||||||
|
|
||||||
|
oidc.setUser({
|
||||||
|
sub: 'test-user',
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await oidc.runSignInUp({
|
||||||
|
state: auth.state,
|
||||||
|
});
|
||||||
|
const meResult = await execute({
|
||||||
|
document: TestMeQuery,
|
||||||
|
authToken: result.accessToken,
|
||||||
|
}).then(r => r.expectNoGraphQLErrors());
|
||||||
|
expect(meResult).toMatchObject({
|
||||||
|
me: {
|
||||||
|
email,
|
||||||
|
id: expect.any(String),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -430,6 +430,8 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args.req.log.debug('the email is verified');
|
||||||
|
|
||||||
args.req.log.debug('SuperTokens session resolved.');
|
args.req.log.debug('SuperTokens session resolved.');
|
||||||
return sessionData;
|
return sessionData;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,26 @@ export class SuperTokensStore {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateOIDCUserEmail(args: { userId: string; newEmail: string }) {
|
||||||
|
const query = sql`
|
||||||
|
UPDATE
|
||||||
|
"supertokens_thirdparty_users"
|
||||||
|
SET
|
||||||
|
"email" = ${args.newEmail}
|
||||||
|
WHERE
|
||||||
|
"app_id" = 'public'
|
||||||
|
AND "user_id" = ${args.userId}
|
||||||
|
RETURNING
|
||||||
|
"user_id" AS "userId"
|
||||||
|
, "email" AS "email"
|
||||||
|
, "third_party_id" AS "thirdPartyId"
|
||||||
|
, "third_party_user_id" AS "thirdPartyUserId"
|
||||||
|
, "time_joined" AS "timeJoined"
|
||||||
|
`;
|
||||||
|
|
||||||
|
return await this.pool.maybeOne(query).then(ThirdpartUserModel.nullable().parse);
|
||||||
|
}
|
||||||
|
|
||||||
async createThirdPartyUser(args: {
|
async createThirdPartyUser(args: {
|
||||||
email: string;
|
email: string;
|
||||||
thirdPartyId: string;
|
thirdPartyId: string;
|
||||||
|
|
|
||||||
|
|
@ -1119,7 +1119,7 @@ export async function registerSupertokensAtHome(
|
||||||
);
|
);
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1130,7 +1130,7 @@ export async function registerSupertokensAtHome(
|
||||||
req.log.debug('received malformed json body from token endpoint');
|
req.log.debug('received malformed json body from token endpoint');
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1140,7 +1140,7 @@ export async function registerSupertokensAtHome(
|
||||||
req.log.debug('received invalid json body from token endpoint');
|
req.log.debug('received invalid json body from token endpoint');
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1231,7 +1231,10 @@ export async function registerSupertokensAtHome(
|
||||||
const current_url = new URL(env.hiveServices.webApp.url);
|
const current_url = new URL(env.hiveServices.webApp.url);
|
||||||
current_url.pathname = '/auth/callback/oidc';
|
current_url.pathname = '/auth/callback/oidc';
|
||||||
|
|
||||||
req.log.debug('attempt exchanging auth code for auth token');
|
req.log.debug(
|
||||||
|
'attempt exchanging auth code for auth token (endpoint=%s)',
|
||||||
|
oidcIntegration.tokenEndpoint,
|
||||||
|
);
|
||||||
|
|
||||||
broadcastLog(
|
broadcastLog(
|
||||||
oidcIntegration.id,
|
oidcIntegration.id,
|
||||||
|
|
@ -1267,7 +1270,7 @@ export async function registerSupertokensAtHome(
|
||||||
|
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1284,7 +1287,7 @@ export async function registerSupertokensAtHome(
|
||||||
|
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1299,7 +1302,7 @@ export async function registerSupertokensAtHome(
|
||||||
|
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1313,6 +1316,7 @@ export async function registerSupertokensAtHome(
|
||||||
authorization: `Bearer ${codeGrantAccessToken}`,
|
authorization: `Bearer ${codeGrantAccessToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const userInfoBodyRaw = await userInfoResponse.text();
|
||||||
|
|
||||||
if (userInfoResponse.status != 200) {
|
if (userInfoResponse.status != 200) {
|
||||||
req.log.debug(
|
req.log.debug(
|
||||||
|
|
@ -1321,16 +1325,15 @@ export async function registerSupertokensAtHome(
|
||||||
);
|
);
|
||||||
broadcastLog(
|
broadcastLog(
|
||||||
oidcIntegration.id,
|
oidcIntegration.id,
|
||||||
`an unexpected error occured while calling the user info endoint '${oidcIntegration.userinfoEndpoint}'. HTTP Status: ${grantResponse.status} Body: ${await grantResponse.text()}.`,
|
`an unexpected error occured while calling the user info endoint '${oidcIntegration.userinfoEndpoint}'. HTTP Status: ${userInfoResponse.status} HTTP Body: ${userInfoBodyRaw}.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userInfoBodyRaw = await userInfoResponse.text();
|
|
||||||
const userInfoBodyJSON = parseJSONSafe(userInfoBodyRaw);
|
const userInfoBodyJSON = parseJSONSafe(userInfoBodyRaw);
|
||||||
|
|
||||||
if (userInfoBodyJSON.type === 'error') {
|
if (userInfoBodyJSON.type === 'error') {
|
||||||
|
|
@ -1342,7 +1345,7 @@ export async function registerSupertokensAtHome(
|
||||||
|
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1362,7 +1365,7 @@ export async function registerSupertokensAtHome(
|
||||||
|
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1375,7 +1378,7 @@ export async function registerSupertokensAtHome(
|
||||||
|
|
||||||
return rep.status(200).send({
|
return rep.status(200).send({
|
||||||
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
reason: 'Sign in failed. Please contact your origanization administrator.',
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1393,6 +1396,19 @@ export async function registerSupertokensAtHome(
|
||||||
oidcIntegrationId: oidcIntegration.id,
|
oidcIntegrationId: oidcIntegration.id,
|
||||||
sub: userInfoBody.data.sub,
|
sub: userInfoBody.data.sub,
|
||||||
});
|
});
|
||||||
|
} else if (user.email !== userInfoBody.data.email) {
|
||||||
|
req.log.debug('providers email has changed. Update record.');
|
||||||
|
user = await supertokensStore.updateOIDCUserEmail({
|
||||||
|
userId: user.userId,
|
||||||
|
newEmail: userInfoBody.data.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return rep.status(200).send({
|
||||||
|
status: 'SIGN_IN_UP_NOT_ALLOWED',
|
||||||
|
reason: 'Sign in failed. Please contact your organization administrator.',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.log.debug('supertokens user provisioned. ensure hive user exists');
|
req.log.debug('supertokens user provisioned. ensure hive user exists');
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@sentry/node": "^7.0.0",
|
"@sentry/node": "^7.0.0",
|
||||||
"@sentry/utils": "^7.0.0",
|
"@sentry/utils": "^7.0.0",
|
||||||
|
|
|
||||||
|
|
@ -758,6 +758,15 @@ export async function createStorage(
|
||||||
action = 'created';
|
action = 'created';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (internalUser.email !== email) {
|
||||||
|
await t.query(sql`
|
||||||
|
UPDATE "users"
|
||||||
|
SET "email" = ${email}
|
||||||
|
WHERE "id" = ${internalUser.id}
|
||||||
|
`);
|
||||||
|
internalUser.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
if (oidcIntegration !== null) {
|
if (oidcIntegration !== null) {
|
||||||
// Add user to OIDC linked integration
|
// Add user to OIDC linked integration
|
||||||
await shared.addOrganizationMemberViaOIDCIntegrationId(
|
await shared.addOrganizationMemberViaOIDCIntegrationId(
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,9 @@ importers:
|
||||||
'@hive/server':
|
'@hive/server':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/services/server
|
version: link:../packages/services/server
|
||||||
|
'@hive/service-common':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/services/service-common
|
||||||
'@hive/storage':
|
'@hive/storage':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/services/storage
|
version: link:../packages/services/storage
|
||||||
|
|
@ -370,6 +373,9 @@ importers:
|
||||||
'@types/dockerode':
|
'@types/dockerode':
|
||||||
specifier: 3.3.43
|
specifier: 3.3.43
|
||||||
version: 3.3.43
|
version: 3.3.43
|
||||||
|
'@types/set-cookie-parser':
|
||||||
|
specifier: 2.4.10
|
||||||
|
version: 2.4.10
|
||||||
async-retry:
|
async-retry:
|
||||||
specifier: 1.3.3
|
specifier: 1.3.3
|
||||||
version: 1.3.3
|
version: 1.3.3
|
||||||
|
|
@ -400,6 +406,9 @@ importers:
|
||||||
ioredis:
|
ioredis:
|
||||||
specifier: 5.8.2
|
specifier: 5.8.2
|
||||||
version: 5.8.2
|
version: 5.8.2
|
||||||
|
set-cookie-parser:
|
||||||
|
specifier: 2.7.1
|
||||||
|
version: 2.7.1
|
||||||
slonik:
|
slonik:
|
||||||
specifier: 30.4.4
|
specifier: 30.4.4
|
||||||
version: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9)
|
version: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9)
|
||||||
|
|
@ -10538,6 +10547,9 @@ packages:
|
||||||
'@types/service-worker-mock@2.0.4':
|
'@types/service-worker-mock@2.0.4':
|
||||||
resolution: {integrity: sha512-MEBT2eiqYfhxjqYm/oAf2AvKLbPTPwJJAYrMdheKnGyz1yG9XBRfxCzi93h27qpSvI7jOYfXqFLVMLBXFDqo4A==}
|
resolution: {integrity: sha512-MEBT2eiqYfhxjqYm/oAf2AvKLbPTPwJJAYrMdheKnGyz1yG9XBRfxCzi93h27qpSvI7jOYfXqFLVMLBXFDqo4A==}
|
||||||
|
|
||||||
|
'@types/set-cookie-parser@2.4.10':
|
||||||
|
resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==}
|
||||||
|
|
||||||
'@types/shimmer@1.2.0':
|
'@types/shimmer@1.2.0':
|
||||||
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
|
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
|
||||||
|
|
||||||
|
|
@ -32881,6 +32893,10 @@ snapshots:
|
||||||
|
|
||||||
'@types/service-worker-mock@2.0.4': {}
|
'@types/service-worker-mock@2.0.4': {}
|
||||||
|
|
||||||
|
'@types/set-cookie-parser@2.4.10':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.10.12
|
||||||
|
|
||||||
'@types/shimmer@1.2.0': {}
|
'@types/shimmer@1.2.0': {}
|
||||||
|
|
||||||
'@types/sinonjs__fake-timers@8.1.1': {}
|
'@types/sinonjs__fake-timers@8.1.1': {}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue