mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Merge branch 'dev' into promptless/document-developer-tools
Some checks failed
DB migration compat / Check if migrations changed (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
Some checks failed
DB migration compat / Check if migrations changed (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
This commit is contained in:
commit
a4ffe8d771
@ -104,6 +104,10 @@ export const GET = createSmartRouteHandler({
|
||||
|
||||
const { turnstileAssessment } = await getRequestContextAndBotChallengeAssessment(query, "oauth_authenticate", tenancy);
|
||||
|
||||
if (query.provider_scope && provider.isShared) {
|
||||
throw new KnownErrors.OAuthExtraScopeNotAvailableWithSharedOAuthKeys();
|
||||
}
|
||||
|
||||
// If a token is provided, store it in the outer info so we can use it to link another user to the account, or to upgrade an anonymous user
|
||||
let projectUserId: string | undefined;
|
||||
if (query.token) {
|
||||
@ -120,9 +124,6 @@ export const GET = createSmartRouteHandler({
|
||||
throw new StatusError(StatusError.Forbidden, "The access token is not valid for this branch");
|
||||
}
|
||||
|
||||
if (query.provider_scope && provider.isShared) {
|
||||
throw new KnownErrors.OAuthExtraScopeNotAvailableWithSharedOAuthKeys();
|
||||
}
|
||||
projectUserId = userId;
|
||||
}
|
||||
|
||||
|
||||
@ -5,10 +5,9 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { connectedAccountAccessTokenCrud } from "@stackframe/stack-shared/dist/interface/crud/connected-accounts";
|
||||
import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
|
||||
import { retrieveOrRefreshAccessToken } from "../../../../access-token-helpers";
|
||||
import { isSharedAccessTokenBlocked, retrieveOrRefreshAccessToken } from "../../../../access-token-helpers";
|
||||
|
||||
|
||||
export const connectedAccountAccessTokenByAccountCrudHandlers = createLazyProxy(() => createCrudHandlers(connectedAccountAccessTokenCrud, {
|
||||
@ -29,7 +28,7 @@ export const connectedAccountAccessTokenByAccountCrudHandlers = createLazyProxy(
|
||||
|
||||
const provider = { id: providerRaw[0], ...providerRaw[1] };
|
||||
|
||||
if (provider.isShared && !getNodeEnvironment().includes('prod') && getEnvVariable('STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS', '') !== 'true') {
|
||||
if (isSharedAccessTokenBlocked(provider.isShared)) {
|
||||
throw new KnownErrors.OAuthAccessTokenNotAvailableWithSharedOAuthKeys();
|
||||
}
|
||||
|
||||
|
||||
@ -5,10 +5,9 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { connectedAccountAccessTokenCrud } from "@stackframe/stack-shared/dist/interface/crud/connected-accounts";
|
||||
import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
|
||||
import { retrieveOrRefreshAccessToken } from "../../../access-token-helpers";
|
||||
import { isSharedAccessTokenBlocked, retrieveOrRefreshAccessToken } from "../../../access-token-helpers";
|
||||
|
||||
|
||||
export const connectedAccountAccessTokenCrudHandlers = createLazyProxy(() => createCrudHandlers(connectedAccountAccessTokenCrud, {
|
||||
@ -28,7 +27,7 @@ export const connectedAccountAccessTokenCrudHandlers = createLazyProxy(() => cre
|
||||
|
||||
const provider = { id: providerRaw[0], ...providerRaw[1] };
|
||||
|
||||
if (provider.isShared && !getNodeEnvironment().includes('prod') && getEnvVariable('STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS', '') !== 'true') {
|
||||
if (isSharedAccessTokenBlocked(provider.isShared)) {
|
||||
throw new KnownErrors.OAuthAccessTokenNotAvailableWithSharedOAuthKeys();
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,64 @@
|
||||
import { OAuthBaseProvider, TokenSet } from "@/oauth/providers/base";
|
||||
import { getPrismaClientForTenancy } from "@/prisma-client";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { Result } from "@stackframe/stack-shared/dist/utils/results";
|
||||
import { extractScopes } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
|
||||
/**
|
||||
* Access tokens minted under Stack Auth's shared OAuth apps must not be handed
|
||||
* to clients — they carry Stack Auth's brand at the provider. Only allowed when
|
||||
* the deployer explicitly opts in via STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS.
|
||||
* NOT gated on NODE_ENV — the env-var opt-in is the only escape hatch.
|
||||
*/
|
||||
export function isSharedAccessTokenBlocked(providerIsShared: boolean): boolean {
|
||||
if (!providerIsShared) return false;
|
||||
return getEnvVariable("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "") !== "true";
|
||||
}
|
||||
|
||||
import.meta.vitest?.describe("isSharedAccessTokenBlocked", () => {
|
||||
const { test, expect, beforeEach, afterEach, vi } = import.meta.vitest!;
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "");
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
test("non-shared provider is never blocked, regardless of env var", () => {
|
||||
expect(isSharedAccessTokenBlocked(false)).toBe(false);
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "true");
|
||||
expect(isSharedAccessTokenBlocked(false)).toBe(false);
|
||||
});
|
||||
|
||||
test("shared provider is blocked when env var is unset or empty", () => {
|
||||
expect(isSharedAccessTokenBlocked(true)).toBe(true);
|
||||
});
|
||||
|
||||
test("shared provider is blocked for any value other than the literal 'true'", () => {
|
||||
for (const v of ["false", "1", "TRUE", "yes", " true "]) {
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", v);
|
||||
expect(isSharedAccessTokenBlocked(true)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("shared provider is allowed only when env var === 'true'", () => {
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "true");
|
||||
expect(isSharedAccessTokenBlocked(true)).toBe(false);
|
||||
});
|
||||
|
||||
test("result does not depend on NODE_ENV", () => {
|
||||
for (const nodeEnv of ["production", "development", "test", "preview", ""]) {
|
||||
vi.stubEnv("NODE_ENV", nodeEnv);
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "");
|
||||
expect(isSharedAccessTokenBlocked(true)).toBe(true);
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "true");
|
||||
expect(isSharedAccessTokenBlocked(true)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieves a valid access token for one or more OAuth accounts, or refreshes one if needed.
|
||||
*
|
||||
|
||||
@ -272,3 +272,55 @@ it("should fail if an untrusted after_callback_redirect_url is provided", async
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
// Regression: provider_scope against a shared provider must be rejected on
|
||||
// every authorize path — not only when a link token is present. A malicious
|
||||
// client would otherwise request elevated scopes under Stack Auth's shared
|
||||
// OAuth app on a plain sign-in.
|
||||
it("should reject provider_scope on shared provider for plain sign-in (no link token)", async ({ expect }) => {
|
||||
const response = await niceBackendFetch("/api/v1/auth/oauth/authorize/spotify", {
|
||||
redirect: "manual",
|
||||
query: {
|
||||
...await Auth.OAuth.getAuthorizeQuery(),
|
||||
provider_scope: "user-read-private user-library-modify playlist-modify-public",
|
||||
},
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 400,
|
||||
"body": {
|
||||
"code": "OAUTH_EXTRA_SCOPE_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS",
|
||||
"error": "Extra scopes are not available with shared OAuth keys. Please add your own OAuth keys on the Stack dashboard to use extra scopes.",
|
||||
},
|
||||
"headers": Headers {
|
||||
"x-stack-known-error": "OAUTH_EXTRA_SCOPE_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS",
|
||||
<some fields may have been hidden>,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should reject provider_scope on shared provider for account-link flow", async ({ expect }) => {
|
||||
await Auth.OAuth.signIn();
|
||||
const response = await niceBackendFetch("/api/v1/auth/oauth/authorize/spotify", {
|
||||
redirect: "manual",
|
||||
query: {
|
||||
...await Auth.OAuth.getAuthorizeQuery(),
|
||||
type: "link",
|
||||
provider_scope: "user-read-private user-library-modify",
|
||||
},
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 400,
|
||||
"body": {
|
||||
"code": "OAUTH_EXTRA_SCOPE_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS",
|
||||
"error": "Extra scopes are not available with shared OAuth keys. Please add your own OAuth keys on the Stack dashboard to use extra scopes.",
|
||||
},
|
||||
"headers": Headers {
|
||||
"x-stack-known-error": "OAUTH_EXTRA_SCOPE_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS",
|
||||
<some fields may have been hidden>,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user