mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Fix OAuth provider disablement
This commit is contained in:
parent
078073b843
commit
936e298032
@ -6,6 +6,7 @@ import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { oauthProviderCrud } from "@stackframe/stack-shared/dist/interface/crud/oauth-providers";
|
||||
import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { allProviders, ProviderType } from "@stackframe/stack-shared/dist/utils/oauth";
|
||||
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
|
||||
|
||||
// Helper function to check if a provider type is already used for signing in
|
||||
@ -107,24 +108,33 @@ async function ensureProviderExists(tenancy: Tenancy, userId: string, providerId
|
||||
return provider;
|
||||
}
|
||||
|
||||
function getProviderConfig(tenancy: Tenancy, providerConfigId: string) {
|
||||
function findProviderConfig(tenancy: Tenancy, providerConfigId: string) {
|
||||
const config = tenancy.config;
|
||||
let providerConfig: (typeof config.auth.oauth.providers)[number] & { id: string } | undefined;
|
||||
for (const [providerId, provider] of Object.entries(config.auth.oauth.providers)) {
|
||||
if (providerId === providerConfigId) {
|
||||
providerConfig = {
|
||||
id: providerId,
|
||||
...provider,
|
||||
};
|
||||
break;
|
||||
return { id: providerId, ...provider };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!providerConfig) {
|
||||
function getProviderConfig(tenancy: Tenancy, providerConfigId: string) {
|
||||
const config = findProviderConfig(tenancy, providerConfigId);
|
||||
if (!config) {
|
||||
throw new StatusError(StatusError.NotFound, `OAuth provider ${providerConfigId} not found or not configured`);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
return providerConfig;
|
||||
function resolveProviderType(tenancy: Tenancy, configOAuthProviderId: string): ProviderType | null {
|
||||
const config = findProviderConfig(tenancy, configOAuthProviderId);
|
||||
if (config?.type != null) {
|
||||
return config.type;
|
||||
}
|
||||
if ((allProviders as readonly string[]).includes(configOAuthProviderId)) {
|
||||
return configOAuthProviderId as ProviderType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -148,14 +158,17 @@ export const oauthProviderCrudHandlers = createLazyProxy(() => createCrudHandler
|
||||
await ensureUserExists(prismaClient, { tenancyId: auth.tenancy.id, userId: params.user_id });
|
||||
const oauthAccount = await ensureProviderExists(auth.tenancy, params.user_id, params.provider_id);
|
||||
|
||||
const providerConfig = getProviderConfig(auth.tenancy, oauthAccount.configOAuthProviderId);
|
||||
const providerType = resolveProviderType(auth.tenancy, oauthAccount.configOAuthProviderId);
|
||||
if (providerType == null) {
|
||||
throw new StatusError(StatusError.NotFound, `OAuth provider ${oauthAccount.configOAuthProviderId} not found or not configured`);
|
||||
}
|
||||
|
||||
return {
|
||||
user_id: params.user_id,
|
||||
id: oauthAccount.id,
|
||||
email: oauthAccount.email || undefined,
|
||||
provider_config_id: oauthAccount.configOAuthProviderId,
|
||||
type: providerConfig.type as any, // Type assertion to match schema
|
||||
type: providerType,
|
||||
allow_sign_in: oauthAccount.allowSignIn,
|
||||
allow_connected_accounts: oauthAccount.allowConnectedAccounts,
|
||||
account_id: oauthAccount.providerAccountId,
|
||||
@ -187,19 +200,22 @@ export const oauthProviderCrudHandlers = createLazyProxy(() => createCrudHandler
|
||||
|
||||
return {
|
||||
items: oauthAccounts
|
||||
.map((oauthAccount) => {
|
||||
const providerConfig = getProviderConfig(auth.tenancy, oauthAccount.configOAuthProviderId);
|
||||
.flatMap((oauthAccount) => {
|
||||
const providerType = resolveProviderType(auth.tenancy, oauthAccount.configOAuthProviderId);
|
||||
if (providerType == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return {
|
||||
return [{
|
||||
user_id: oauthAccount.projectUserId || throwErr("OAuth account has no project user ID"),
|
||||
id: oauthAccount.id,
|
||||
email: oauthAccount.email || undefined,
|
||||
provider_config_id: oauthAccount.configOAuthProviderId,
|
||||
type: providerConfig.type as any, // Type assertion to match schema
|
||||
type: providerType,
|
||||
allow_sign_in: oauthAccount.allowSignIn,
|
||||
allow_connected_accounts: oauthAccount.allowConnectedAccounts,
|
||||
account_id: oauthAccount.providerAccountId,
|
||||
};
|
||||
}];
|
||||
}),
|
||||
is_paginated: false,
|
||||
};
|
||||
@ -299,14 +315,15 @@ export const oauthProviderCrudHandlers = createLazyProxy(() => createCrudHandler
|
||||
},
|
||||
});
|
||||
|
||||
const providerConfig = getProviderConfig(auth.tenancy, existingOAuthAccount.configOAuthProviderId);
|
||||
const providerType = resolveProviderType(auth.tenancy, existingOAuthAccount.configOAuthProviderId)
|
||||
?? throwErr(new StatusError(StatusError.NotFound, `OAuth provider ${existingOAuthAccount.configOAuthProviderId} not found or not configured`));
|
||||
|
||||
return {
|
||||
user_id: params.user_id,
|
||||
id: params.provider_id,
|
||||
email: data.email ?? existingOAuthAccount.email ?? undefined,
|
||||
provider_config_id: existingOAuthAccount.configOAuthProviderId,
|
||||
type: providerConfig.type as any,
|
||||
type: providerType,
|
||||
allow_sign_in: data.allow_sign_in ?? existingOAuthAccount.allowSignIn,
|
||||
allow_connected_accounts: data.allow_connected_accounts ?? existingOAuthAccount.allowConnectedAccounts,
|
||||
account_id: data.account_id ?? existingOAuthAccount.providerAccountId,
|
||||
|
||||
@ -1027,3 +1027,138 @@ it("should not allow get, update, delete oauth providers with wrong user id and
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should still list and read OAuth providers after the provider is disabled on the project", async ({ expect }) => {
|
||||
const { adminAccessToken, createProjectResponse } = await createAndSwitchToOAuthEnabledProject();
|
||||
await Auth.fastSignUp();
|
||||
|
||||
const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify");
|
||||
expect(providerConfig).toBeDefined();
|
||||
|
||||
const createResponse = await niceBackendFetch("/api/v1/oauth-providers", {
|
||||
method: "POST",
|
||||
accessType: "server",
|
||||
body: {
|
||||
user_id: "me",
|
||||
provider_config_id: providerConfig.id,
|
||||
account_id: "test_spotify_user_123",
|
||||
email: "test@example.com",
|
||||
allow_sign_in: true,
|
||||
allow_connected_accounts: true,
|
||||
},
|
||||
});
|
||||
expect(createResponse.status).toBe(201);
|
||||
const providerId = createResponse.body.id;
|
||||
|
||||
// Disable the OAuth provider on the project
|
||||
await Project.updateCurrent(adminAccessToken, {
|
||||
config: {
|
||||
oauth_providers: [],
|
||||
},
|
||||
});
|
||||
|
||||
// List should still work and include the provider
|
||||
const listResponse = await niceBackendFetch("/api/v1/oauth-providers?user_id=me", {
|
||||
method: "GET",
|
||||
accessType: "server",
|
||||
});
|
||||
expect(listResponse).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"is_paginated": false,
|
||||
"items": [
|
||||
{
|
||||
"account_id": "test_spotify_user_123",
|
||||
"allow_connected_accounts": true,
|
||||
"allow_sign_in": true,
|
||||
"email": "test@example.com",
|
||||
"id": "<stripped UUID>",
|
||||
"provider_config_id": "spotify",
|
||||
"type": "spotify",
|
||||
"user_id": "<stripped UUID>",
|
||||
},
|
||||
],
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
|
||||
// Read should still work
|
||||
const readResponse = await niceBackendFetch(`/api/v1/oauth-providers/me/${providerId}`, {
|
||||
method: "GET",
|
||||
accessType: "server",
|
||||
});
|
||||
expect(readResponse).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"account_id": "test_spotify_user_123",
|
||||
"allow_connected_accounts": true,
|
||||
"allow_sign_in": true,
|
||||
"email": "test@example.com",
|
||||
"id": "<stripped UUID>",
|
||||
"provider_config_id": "spotify",
|
||||
"type": "spotify",
|
||||
"user_id": "<stripped UUID>",
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
|
||||
// Delete should still work
|
||||
const deleteResponse = await niceBackendFetch(`/api/v1/oauth-providers/me/${providerId}`, {
|
||||
method: "DELETE",
|
||||
accessType: "server",
|
||||
});
|
||||
expect(deleteResponse).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": { "success": true },
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
|
||||
// After delete, list should return empty
|
||||
const listAfterDelete = await niceBackendFetch("/api/v1/oauth-providers?user_id=me", {
|
||||
method: "GET",
|
||||
accessType: "server",
|
||||
});
|
||||
expect(listAfterDelete.body.items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should still return the user when their OAuth provider is disabled on the project", async ({ expect }) => {
|
||||
const { adminAccessToken, createProjectResponse } = await createAndSwitchToOAuthEnabledProject();
|
||||
const { userId } = await Auth.fastSignUp();
|
||||
|
||||
const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify");
|
||||
expect(providerConfig).toBeDefined();
|
||||
|
||||
await niceBackendFetch("/api/v1/oauth-providers", {
|
||||
method: "POST",
|
||||
accessType: "server",
|
||||
body: {
|
||||
user_id: "me",
|
||||
provider_config_id: providerConfig.id,
|
||||
account_id: "test_spotify_user_123",
|
||||
email: "test@example.com",
|
||||
allow_sign_in: true,
|
||||
allow_connected_accounts: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Disable the OAuth provider on the project
|
||||
await Project.updateCurrent(adminAccessToken, {
|
||||
config: {
|
||||
oauth_providers: [],
|
||||
},
|
||||
});
|
||||
|
||||
// User endpoint should still work
|
||||
const userResponse = await niceBackendFetch(`/api/v1/users/${userId}`, {
|
||||
method: "GET",
|
||||
accessType: "server",
|
||||
});
|
||||
expect(userResponse.status).toBe(200);
|
||||
expect(userResponse.body.id).toBe(userId);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user