mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Mock OAuth server (#138)
This commit is contained in:
parent
fd6f6c6d93
commit
84960ec9ca
9
.github/workflows/e2e-api-tests.yaml
vendored
9
.github/workflows/e2e-api-tests.yaml
vendored
@ -78,6 +78,15 @@ jobs:
|
||||
tail: true
|
||||
wait-for: 30s
|
||||
log-output-if: true
|
||||
- name: Start oauth-mock-server in background
|
||||
uses: JarvusInnovations/background-action@v1.0.7
|
||||
with:
|
||||
run: pnpm run start:oauth-mock-server --log-order=stream &
|
||||
wait-on: |
|
||||
http://localhost:8102
|
||||
tail: true
|
||||
wait-for: 30s
|
||||
log-output-if: true
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -23,6 +23,7 @@
|
||||
"nextjs",
|
||||
"Nicifiable",
|
||||
"nicify",
|
||||
"oidc",
|
||||
"openapi",
|
||||
"Proxied",
|
||||
"reqs",
|
||||
|
||||
@ -2,8 +2,11 @@
|
||||
STACK_BASE_URL=# enter the URL of the backend here. For local development, use `http://localhost:8102`.
|
||||
STACK_SERVER_SECRET=# enter a secret key generated by `pnpm generate-keys` here. This is used to sign the JWT tokens.
|
||||
|
||||
# OAuth mock provider settings
|
||||
STACK_OAUTH_MOCK_URL=# enter the URL of the mock OAuth provider here. For local development, use `http://localhost:8107`.
|
||||
|
||||
# OAuth shared keys
|
||||
# Can be omitted if shared OAuth keys are not needed
|
||||
# Can be set to MOCK to use mock OAuth providers
|
||||
STACK_GITHUB_CLIENT_ID=# client
|
||||
STACK_GITHUB_CLIENT_SECRET=# client secret
|
||||
STACK_GOOGLE_CLIENT_ID=# client id
|
||||
|
||||
@ -1,6 +1,19 @@
|
||||
STACK_BASE_URL=http://localhost:8102
|
||||
STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
|
||||
|
||||
STACK_OAUTH_MOCK_URL=http://localhost:8107
|
||||
|
||||
STACK_GITHUB_CLIENT_ID=MOCK
|
||||
STACK_GITHUB_CLIENT_SECRET=MOCK
|
||||
STACK_GOOGLE_CLIENT_ID=MOCK
|
||||
STACK_GOOGLE_CLIENT_SECRET=MOCK
|
||||
STACK_FACEBOOK_CLIENT_ID=MOCK
|
||||
STACK_FACEBOOK_CLIENT_SECRET=MOCK
|
||||
STACK_MICROSOFT_CLIENT_ID=MOCK
|
||||
STACK_MICROSOFT_CLIENT_SECRET=MOCK
|
||||
STACK_SPOTIFY_CLIENT_ID=MOCK
|
||||
STACK_SPOTIFY_CLIENT_SECRET=MOCK
|
||||
|
||||
STACK_DATABASE_CONNECTION_STRING=postgres://postgres:password@localhost:5432/stackframe
|
||||
STACK_DIRECT_DATABASE_CONNECTION_STRING=postgres://postgres:password@localhost:5432/stackframe
|
||||
|
||||
|
||||
@ -83,7 +83,8 @@ export const GET = createSmartRouteHandler({
|
||||
|
||||
const innerCodeVerifier = generators.codeVerifier();
|
||||
const innerState = generators.state();
|
||||
const oauthUrl = getProvider(provider).getAuthorizationUrl({
|
||||
const providerObj = await getProvider(provider);
|
||||
const oauthUrl = providerObj.getAuthorizationUrl({
|
||||
codeVerifier: innerCodeVerifier,
|
||||
state: innerState,
|
||||
extraScope: query.provider_scope,
|
||||
@ -115,7 +116,7 @@ export const GET = createSmartRouteHandler({
|
||||
// prevent CSRF by keeping track of the inner state in cookies
|
||||
// the callback route must ensure that the inner state cookie is set
|
||||
cookies().set(
|
||||
"stack-oauth-" + innerState,
|
||||
"stack-oauth-inner-" + innerState,
|
||||
"true",
|
||||
{
|
||||
httpOnly: true,
|
||||
|
||||
@ -39,8 +39,9 @@ export const GET = createSmartRouteHandler({
|
||||
headers: yupMixed().required(),
|
||||
}),
|
||||
async handler({ params, query }, fullReq) {
|
||||
const cookieInfo = cookies().get("stack-oauth-" + query.state);
|
||||
cookies().delete("stack-oauth-" + query.state);
|
||||
const innerState = query.state ?? "";
|
||||
const cookieInfo = cookies().get("stack-oauth-inner-" + innerState);
|
||||
cookies().delete("stack-oauth-inner-" + query.state);
|
||||
|
||||
if (cookieInfo?.value !== 'true') {
|
||||
throw new StatusError(StatusError.BadRequest, "stack-oauth cookie not found");
|
||||
@ -88,13 +89,11 @@ export const GET = createSmartRouteHandler({
|
||||
throw new KnownErrors.OAuthProviderNotFoundOrNotEnabled();
|
||||
}
|
||||
|
||||
const userInfo = await getProvider(provider).getCallback({
|
||||
const providerObj = await getProvider(provider);
|
||||
const userInfo = await providerObj.getCallback({
|
||||
codeVerifier: innerCodeVerifier,
|
||||
state: query.state ?? throwErr(new StatusError(StatusError.BadRequest, "Must provide state in query")),
|
||||
callbackParams: {
|
||||
code: query.code ?? throwErr(new StatusError(StatusError.BadRequest, "Must provide code in query")),
|
||||
state: query.state ?? throwErr(new StatusError(StatusError.BadRequest, "Must provide state in query")),
|
||||
}
|
||||
state: innerState,
|
||||
callbackParams: query,
|
||||
});
|
||||
|
||||
if (type === "link") {
|
||||
@ -152,7 +151,7 @@ export const GET = createSmartRouteHandler({
|
||||
oAuthProviderConfigId: provider.id,
|
||||
refreshToken: userInfo.refreshToken,
|
||||
providerAccountId: userInfo.accountId,
|
||||
scopes: extractScopes(getProvider(provider).scope + " " + providerScope),
|
||||
scopes: extractScopes(providerObj.scope + " " + providerScope),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import OAuth2Server from "@node-oauth/oauth2-server";
|
||||
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { OAuthModel } from "./model";
|
||||
import { OAuthBaseProvider } from "./providers/base";
|
||||
import { FacebookProvider } from "./providers/facebook";
|
||||
@ -9,6 +9,7 @@ import { GithubProvider } from "./providers/github";
|
||||
import { GoogleProvider } from "./providers/google";
|
||||
import { MicrosoftProvider } from "./providers/microsoft";
|
||||
import { SpotifyProvider } from "./providers/spotify";
|
||||
import { MockProvider } from "./providers/mock";
|
||||
|
||||
const _providers = {
|
||||
github: GithubProvider,
|
||||
@ -18,6 +19,8 @@ const _providers = {
|
||||
spotify: SpotifyProvider,
|
||||
} as const;
|
||||
|
||||
const mockProvider = MockProvider;
|
||||
|
||||
const _getEnvForProvider = (provider: keyof typeof _providers) => {
|
||||
return {
|
||||
clientId: getEnvVariable(`STACK_${provider.toUpperCase()}_CLIENT_ID`),
|
||||
@ -25,14 +28,23 @@ const _getEnvForProvider = (provider: keyof typeof _providers) => {
|
||||
};
|
||||
};
|
||||
|
||||
export function getProvider(provider: ProjectsCrud['Admin']['Read']['config']['oauth_providers'][number]): OAuthBaseProvider {
|
||||
export async function getProvider(provider: ProjectsCrud['Admin']['Read']['config']['oauth_providers'][number]): Promise<OAuthBaseProvider> {
|
||||
if (provider.type === 'shared') {
|
||||
return new _providers[provider.id]({
|
||||
clientId: _getEnvForProvider(provider.id).clientId,
|
||||
clientSecret: _getEnvForProvider(provider.id).clientSecret,
|
||||
});
|
||||
const clientId = _getEnvForProvider(provider.id).clientId;
|
||||
const clientSecret = _getEnvForProvider(provider.id).clientSecret;
|
||||
if (clientId === "MOCK") {
|
||||
if (clientSecret !== "MOCK") {
|
||||
throw new StackAssertionError("If OAuth provider client ID is set to MOCK, then client secret must also be set to MOCK");
|
||||
}
|
||||
return await mockProvider.create(provider.id);
|
||||
} else {
|
||||
return await _providers[provider.id].create({
|
||||
clientId,
|
||||
clientSecret,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return new _providers[provider.id]({
|
||||
return await _providers[provider.id].create({
|
||||
clientId: provider.client_id || throwErr("Client ID is required for standard providers"),
|
||||
clientSecret: provider.client_secret || throwErr("Client secret is required for standard providers"),
|
||||
});
|
||||
|
||||
@ -4,28 +4,39 @@ import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"
|
||||
import { mergeScopeStrings } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
|
||||
export abstract class OAuthBaseProvider {
|
||||
issuer: Issuer;
|
||||
scope: string;
|
||||
oauthClient: Client;
|
||||
redirectUri: string;
|
||||
constructor(
|
||||
public readonly oauthClient: Client,
|
||||
public readonly scope: string,
|
||||
public readonly redirectUri: string,
|
||||
) {}
|
||||
|
||||
constructor(options: {
|
||||
issuer: string,
|
||||
authorizationEndpoint: string,
|
||||
tokenEndpoint: string,
|
||||
userinfoEndpoint?: string,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
redirectUri: string,
|
||||
baseScope: string,
|
||||
}) {
|
||||
this.issuer = new Issuer({
|
||||
protected static async createConstructorArgs(options:
|
||||
& {
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
redirectUri: string,
|
||||
baseScope: string,
|
||||
isMock?: boolean,
|
||||
}
|
||||
& (
|
||||
| {
|
||||
issuer: string,
|
||||
authorizationEndpoint: string,
|
||||
tokenEndpoint: string,
|
||||
userinfoEndpoint?: string,
|
||||
}
|
||||
| {
|
||||
discoverFromUrl: string,
|
||||
}
|
||||
)
|
||||
) {
|
||||
const issuer = "discoverFromUrl" in options ? await Issuer.discover(options.discoverFromUrl) : new Issuer({
|
||||
issuer: options.issuer,
|
||||
authorization_endpoint: options.authorizationEndpoint,
|
||||
token_endpoint: options.tokenEndpoint,
|
||||
userinfo_endpoint: options.userinfoEndpoint,
|
||||
});
|
||||
this.oauthClient = new this.issuer.Client({
|
||||
const oauthClient = new issuer.Client({
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret,
|
||||
redirect_uri: options.redirectUri,
|
||||
@ -33,21 +44,20 @@ export abstract class OAuthBaseProvider {
|
||||
});
|
||||
|
||||
// facebook always return an id_token even in the OAuth2 flow, which is not supported by openid-client
|
||||
const oldGrant = this.oauthClient.grant;
|
||||
const oldGrant = oauthClient.grant;
|
||||
if (!(oldGrant as any)) {
|
||||
// it seems that on Sentry, this was undefined in one scenario, so let's log some data to help debug if it happens again
|
||||
// not sure if that is actually what was going on? the error log has very few details
|
||||
// https://stackframe-pw.sentry.io/issues/5515577938
|
||||
throw new StackAssertionError("oldGrant is undefined for some reason — that should never happen!", { options, oauthClient: this.oauthClient });
|
||||
throw new StackAssertionError("oldGrant is undefined for some reason — that should never happen!", { options, oauthClient });
|
||||
}
|
||||
this.oauthClient.grant = async function (params) {
|
||||
oauthClient.grant = async function (params) {
|
||||
const grant = await oldGrant.call(this, params);
|
||||
delete grant.id_token;
|
||||
return grant;
|
||||
};
|
||||
|
||||
this.redirectUri = options.redirectUri;
|
||||
this.scope = options.baseScope;
|
||||
return [oauthClient, options.baseScope, options.redirectUri] as const;
|
||||
}
|
||||
|
||||
getAuthorizationUrl(options: {
|
||||
@ -71,14 +81,14 @@ export abstract class OAuthBaseProvider {
|
||||
state: string,
|
||||
}): Promise<OAuthUserInfo> {
|
||||
let tokenSet;
|
||||
const params = {
|
||||
code_verifier: options.codeVerifier,
|
||||
state: options.state,
|
||||
};
|
||||
try {
|
||||
const params = {
|
||||
code_verifier: options.codeVerifier,
|
||||
state: options.state,
|
||||
};
|
||||
tokenSet = await this.oauthClient.oauthCallback(this.redirectUri, options.callbackParams, params);
|
||||
} catch (error) {
|
||||
throw new StackAssertionError("OAuth callback failed", undefined, { cause: error });
|
||||
throw new StackAssertionError(`Inner OAuth callback failed due to error: ${error}`, undefined, { cause: error });
|
||||
}
|
||||
if (!tokenSet.access_token) {
|
||||
throw new StackAssertionError("No access token received", { tokenSet });
|
||||
|
||||
@ -4,18 +4,24 @@ import { OAuthUserInfo, validateUserInfo } from "../utils";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
|
||||
export class FacebookProvider extends OAuthBaseProvider {
|
||||
constructor(options: {
|
||||
private constructor(
|
||||
...args: ConstructorParameters<typeof OAuthBaseProvider>
|
||||
) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
static async create(options: {
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
}) {
|
||||
super({
|
||||
return new FacebookProvider(...await OAuthBaseProvider.createConstructorArgs({
|
||||
issuer: "https://www.facebook.com",
|
||||
authorizationEndpoint: "https://facebook.com/v20.0/dialog/oauth/",
|
||||
tokenEndpoint: "https://graph.facebook.com/v20.0/oauth/access_token",
|
||||
redirectUri: getEnvVariable("STACK_BASE_URL") + "/api/v1/auth/oauth/callback/facebook",
|
||||
baseScope: "public_profile email",
|
||||
...options
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
|
||||
|
||||
@ -4,11 +4,17 @@ import { OAuthUserInfo, validateUserInfo } from "../utils";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
|
||||
export class GithubProvider extends OAuthBaseProvider {
|
||||
constructor(options: {
|
||||
private constructor(
|
||||
...args: ConstructorParameters<typeof OAuthBaseProvider>
|
||||
) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
static async create(options: {
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
}) {
|
||||
super({
|
||||
return new GithubProvider(...await OAuthBaseProvider.createConstructorArgs({
|
||||
issuer: "https://github.com",
|
||||
authorizationEndpoint: "https://github.com/login/oauth/authorize",
|
||||
tokenEndpoint: "https://github.com/login/oauth/access_token",
|
||||
@ -16,7 +22,7 @@ export class GithubProvider extends OAuthBaseProvider {
|
||||
redirectUri: getEnvVariable("STACK_BASE_URL") + "/api/v1/auth/oauth/callback/github",
|
||||
baseScope: "user:email",
|
||||
...options,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
|
||||
|
||||
@ -4,11 +4,17 @@ import { OAuthUserInfo, validateUserInfo } from "../utils";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
|
||||
export class GoogleProvider extends OAuthBaseProvider {
|
||||
constructor(options: {
|
||||
private constructor(
|
||||
...args: ConstructorParameters<typeof OAuthBaseProvider>
|
||||
) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
static async create(options: {
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
}) {
|
||||
super({
|
||||
return new GoogleProvider(...await OAuthBaseProvider.createConstructorArgs({
|
||||
issuer: "https://accounts.google.com",
|
||||
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
||||
@ -16,7 +22,7 @@ export class GoogleProvider extends OAuthBaseProvider {
|
||||
redirectUri: getEnvVariable("STACK_BASE_URL") + "/api/v1/auth/oauth/callback/google",
|
||||
baseScope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile",
|
||||
...options,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
|
||||
|
||||
@ -4,18 +4,24 @@ import { OAuthUserInfo, validateUserInfo } from "../utils";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
|
||||
export class MicrosoftProvider extends OAuthBaseProvider {
|
||||
constructor(options: {
|
||||
private constructor(
|
||||
...args: ConstructorParameters<typeof OAuthBaseProvider>
|
||||
) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
static async create(options: {
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
}) {
|
||||
super({
|
||||
return new MicrosoftProvider(...await OAuthBaseProvider.createConstructorArgs({
|
||||
issuer: "https://login.microsoftonline.com",
|
||||
authorizationEndpoint: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize",
|
||||
tokenEndpoint: "https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
|
||||
redirectUri: getEnvVariable("STACK_BASE_URL") + "/api/v1/auth/oauth/callback/microsoft",
|
||||
baseScope: "User.Read",
|
||||
...options,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
|
||||
|
||||
36
apps/backend/src/oauth/providers/mock.tsx
Normal file
36
apps/backend/src/oauth/providers/mock.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { TokenSet } from "openid-client";
|
||||
import { OAuthBaseProvider } from "./base";
|
||||
import { OAuthUserInfo, validateUserInfo } from "../utils";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
|
||||
export class MockProvider extends OAuthBaseProvider {
|
||||
constructor(
|
||||
...args: ConstructorParameters<typeof OAuthBaseProvider>
|
||||
) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
static async create(providerId: string) {
|
||||
return new MockProvider(...await OAuthBaseProvider.createConstructorArgs({
|
||||
discoverFromUrl: getEnvVariable("STACK_OAUTH_MOCK_URL"),
|
||||
redirectUri: `${getEnvVariable("STACK_BASE_URL")}/api/v1/auth/oauth/callback/${providerId}`,
|
||||
baseScope: "openid",
|
||||
isMock: true,
|
||||
clientId: providerId,
|
||||
clientSecret: "MOCK-SERVER-SECRET",
|
||||
}));
|
||||
}
|
||||
|
||||
async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
|
||||
const rawUserInfo = await this.oauthClient.userinfo(tokenSet);
|
||||
|
||||
return validateUserInfo({
|
||||
accountId: rawUserInfo.sub,
|
||||
displayName: rawUserInfo.name,
|
||||
email: rawUserInfo.sub,
|
||||
profileImageUrl: rawUserInfo.picture,
|
||||
accessToken: tokenSet.access_token,
|
||||
refreshToken: tokenSet.refresh_token,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -4,18 +4,24 @@ import { OAuthUserInfo, validateUserInfo } from "../utils";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
|
||||
export class SpotifyProvider extends OAuthBaseProvider {
|
||||
constructor(options: {
|
||||
private constructor(
|
||||
...args: ConstructorParameters<typeof OAuthBaseProvider>
|
||||
) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
static async create(options: {
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
}) {
|
||||
super({
|
||||
return new SpotifyProvider(...await OAuthBaseProvider.createConstructorArgs({
|
||||
issuer: "https://accounts.spotify.com",
|
||||
authorizationEndpoint: "https://accounts.spotify.com/authorize",
|
||||
tokenEndpoint: "https://accounts.spotify.com/api/token",
|
||||
redirectUri: getEnvVariable("STACK_BASE_URL") + "/api/v1/auth/oauth/callback/spotify",
|
||||
baseScope: "user-read-email user-read-private",
|
||||
...options,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
|
||||
|
||||
@ -129,7 +129,7 @@ export const GET = deprecatedSmartRouteHandler(async (req: NextRequest, options:
|
||||
});
|
||||
|
||||
cookies().set(
|
||||
"stack-oauth-" + innerState.slice(0, 8),
|
||||
"stack-oauth-inner-" + innerState.slice(0, 8),
|
||||
outerInfo.id,
|
||||
{
|
||||
httpOnly: true,
|
||||
|
||||
@ -45,8 +45,8 @@ export const GET = deprecatedSmartRouteHandler(async (req: NextRequest, options:
|
||||
|
||||
const providerId = options.params.provider;
|
||||
|
||||
const infoId = cookies().get("stack-oauth-" + state.slice(0, 8));
|
||||
cookies().delete("stack-oauth-" + state.slice(0, 8));
|
||||
const infoId = cookies().get("stack-oauth-inner-" + state.slice(0, 8));
|
||||
cookies().delete("stack-oauth-inner-" + state.slice(0, 8));
|
||||
|
||||
if (!infoId) {
|
||||
throw new StatusError(StatusError.BadRequest, "stack-oauth cookie not found");
|
||||
|
||||
@ -2,8 +2,6 @@ import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { it, localRedirectUrl } from "../../../../../../helpers";
|
||||
import { backendContext, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
|
||||
// TODO: We need to mock STACK_GITHUB_CLIENT_ID and STACK_GITHUB_CLIENT_SECRET before we can run these tests, so they're currently marked as todo
|
||||
|
||||
function getAuthorizeQuery() {
|
||||
const projectKeys = backendContext.value.projectKeys;
|
||||
if (projectKeys === "no-project") throw new Error("No project keys found in the backend context");
|
||||
@ -21,20 +19,20 @@ function getAuthorizeQuery() {
|
||||
};
|
||||
}
|
||||
|
||||
it.todo("should redirect the user to the OAuth provider with the right arguments", async ({ expect }) => {
|
||||
const response = await niceBackendFetch("/api/v1/auth/oauth/authorize/github", {
|
||||
it("should redirect the user to the OAuth provider with the right arguments", async ({ expect }) => {
|
||||
const response = await niceBackendFetch("/api/v1/auth/oauth/authorize/facebook", {
|
||||
redirect: "manual",
|
||||
query: {
|
||||
...getAuthorizeQuery(),
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(307);
|
||||
expect(response.headers.get("location")).toMatch(/^https:\/\/github\.com\/login\/oauth\/authorize\?.*$/);
|
||||
expect(response.headers.get("set-cookie")).toMatch(/^stack-oauth-[^;]+=[^;]+; Path=\/; Expires=[^;]+; Max-Age=\d+; Secure; HttpOnly$/);
|
||||
expect(response.headers.get("location")).toMatch(/^http:\/\/localhost:8107\/auth\?.*$/);
|
||||
expect(response.headers.get("set-cookie")).toMatch(/^stack-oauth-inner-[^;]+=[^;]+; Path=\/; Expires=[^;]+; Max-Age=\d+; (Secure;)? HttpOnly$/);
|
||||
});
|
||||
|
||||
it.todo("should fail if an invalid client_id is provided", async ({ expect }) => {
|
||||
const response = await niceBackendFetch("/api/v1/auth/oauth/authorize/github", {
|
||||
it("should fail if an invalid client_id is provided", async ({ expect }) => {
|
||||
const response = await niceBackendFetch("/api/v1/auth/oauth/authorize/facebook", {
|
||||
redirect: "manual",
|
||||
query: {
|
||||
...getAuthorizeQuery(),
|
||||
@ -57,8 +55,8 @@ it.todo("should fail if an invalid client_id is provided", async ({ expect }) =>
|
||||
`);
|
||||
});
|
||||
|
||||
it.todo("should fail if an invalid client_secret is provided", async ({ expect }) => {
|
||||
const response = await niceBackendFetch("/api/v1/auth/oauth/authorize/github", {
|
||||
it("should fail if an invalid client_secret is provided", async ({ expect }) => {
|
||||
const response = await niceBackendFetch("/api/v1/auth/oauth/authorize/facebook", {
|
||||
redirect: "manual",
|
||||
query: {
|
||||
...getAuthorizeQuery(),
|
||||
|
||||
18
apps/oauth-mock-server/package.json
Normal file
18
apps/oauth-mock-server/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@stackframe/oauth-mock-server",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "tsx src/index.ts",
|
||||
"dev": "tsx watch --clear-screen=false src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@stackframe/stack-shared": "workspace:*",
|
||||
"oidc-provider": "^8.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.16.2"
|
||||
}
|
||||
}
|
||||
27
apps/oauth-mock-server/src/index.ts
Normal file
27
apps/oauth-mock-server/src/index.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import Provider, { Configuration } from 'oidc-provider';
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || "8107");
|
||||
|
||||
const mockedProviders = [
|
||||
"github",
|
||||
"facebook",
|
||||
"google",
|
||||
"microsoft",
|
||||
"spotify",
|
||||
];
|
||||
|
||||
const configuration: Configuration = {
|
||||
clients: mockedProviders.map((providerId) => ({
|
||||
client_id: providerId,
|
||||
client_secret: 'MOCK-SERVER-SECRET',
|
||||
redirect_uris: [
|
||||
`http://localhost:8102/api/v1/auth/oauth/callback/${providerId}`,
|
||||
],
|
||||
})),
|
||||
};
|
||||
|
||||
const oidc = new Provider(`http://localhost:${port}`, configuration);
|
||||
|
||||
oidc.listen(port, () => {
|
||||
console.log(`oidc-provider listening on port ${port}, check http://localhost:${port}/.well-known/openid-configuration`);
|
||||
});
|
||||
37
apps/oauth-mock-server/tsconfig.json
Normal file
37
apps/oauth-mock-server/tsconfig.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"noErrorTruncation": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@ -3,9 +3,9 @@
|
||||
"version": "2.5.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 8107",
|
||||
"dev": "next dev --port 8112",
|
||||
"build": "next build",
|
||||
"start": "next start --port 8107",
|
||||
"start": "next start --port 8112",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"start": "only-allow pnpm && turbo run start --concurrency 99999",
|
||||
"start:backend": "only-allow pnpm && turbo run start --concurrency 99999 --filter=@stackframe/stack-backend",
|
||||
"start:dashboard": "only-allow pnpm && turbo run start --concurrency 99999 --filter=@stackframe/stack-dashboard",
|
||||
"start:oauth-mock-server": "only-allow pnpm && turbo run start --concurrency 99999 --filter=@stackframe/oauth-mock-server",
|
||||
"lint": "only-allow pnpm && turbo run lint -- --max-warnings=0",
|
||||
"release": "only-allow pnpm && release",
|
||||
"peek": "only-allow pnpm && pnpm release --peek",
|
||||
|
||||
@ -133,6 +133,7 @@ class RetryError extends AggregateError {
|
||||
return this.errors.length;
|
||||
}
|
||||
}
|
||||
RetryError.prototype.name = "RetryError";
|
||||
|
||||
async function retry<T>(
|
||||
fn: () => Result<T> | Promise<Result<T>>,
|
||||
|
||||
613
pnpm-lock.yaml
613
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user