Skip to content

Commit 54027d5

Browse files
fomalhautbN2D4
andauthored
New client (stack-auth#135)
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
1 parent a790dfa commit 54027d5

File tree

362 files changed

+5868
-2860
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

362 files changed

+5868
-2860
lines changed

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"nicify",
2626
"openapi",
2727
"Proxied",
28+
"reqs",
2829
"stackframe",
2930
"typehack",
3031
"Uncapitalize",
@@ -35,5 +36,6 @@
3536
"editor.codeActionsOnSave": {
3637
"source.organizeImports": "explicit"
3738
}
38-
}
39+
},
40+
"terminal.integrated.wordSeparators": " (){}',\"`─‘’“”|"
3941
}

apps/backend/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Basic
2+
STACK_BASE_URL=# enter the URL of the backend here. For local development, use `http://localhost:8102`.
23
STACK_SERVER_SECRET=# enter a secret key generated by `pnpm generate-keys` here. This is used to sign the JWT tokens.
34

45
# OAuth shared keys

apps/backend/.env.development

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1+
STACK_BASE_URL=http://localhost:8102
12
STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
23

3-
NEXT_PUBLIC_STACK_BACKEND_URL=http://localhost:8102
4-
54
STACK_DATABASE_CONNECTION_STRING=postgres://postgres:password@localhost:5432/stackframe
65
STACK_DIRECT_DATABASE_CONNECTION_STRING=postgres://postgres:password@localhost:5432/stackframe
76

apps/backend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
"@stackframe/stack-emails": "workspace:*",
3636
"@vercel/analytics": "^1.2.2",
3737
"bcrypt": "^5.1.1",
38-
"date-fns": "^3.6.0",
3938
"dotenv-cli": "^7.3.0",
4039
"handlebars": "^4.7.8",
4140
"jose": "^5.2.2",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
2+
import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
3+
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
4+
5+
const handler = createSmartRouteHandler({
6+
request: yupObject({
7+
url: yupString().required(),
8+
}),
9+
response: yupObject({
10+
statusCode: yupNumber().oneOf([404]).required(),
11+
bodyType: yupString().oneOf(["text"]).required(),
12+
body: yupString().required(),
13+
}),
14+
handler: async (req, fullReq) => {
15+
return {
16+
statusCode: 404,
17+
bodyType: "text",
18+
body: deindent`
19+
404 — this page does not exist in Stack Auth's API.
20+
21+
Did you mean to visit https://app.stack-auth.com?
22+
23+
URL: ${req.url}
24+
`,
25+
};
26+
},
27+
});
28+
29+
export const GET = handler;
30+
export const POST = handler;
31+
export const PUT = handler;
32+
export const DELETE = handler;
33+
export const PATCH = handler;
34+
export const OPTIONS = handler;
35+
export const HEAD = handler;

apps/backend/src/app/api/v1/auth/oauth/authorize/[provider]/route.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import * as yup from "yup";
1+
import { checkApiKeySet } from "@/lib/api-keys";
2+
import { getProject } from "@/lib/projects";
3+
import { decodeAccessToken, oauthCookieSchema } from "@/lib/tokens";
4+
import { getProvider } from "@/oauth";
25
import { prismaClient } from "@/prisma-client";
36
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
4-
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
7+
import { sharedProviders } from "@stackframe/stack-shared/dist/interface/clientInterface";
58
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
69
import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
7-
import { sharedProviders } from "@stackframe/stack-shared/dist/interface/clientInterface";
8-
import { generators } from "openid-client";
9-
import { getProvider } from "@/oauth";
10-
import { decodeAccessToken, oauthCookieSchema } from "@/lib/tokens";
10+
import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
11+
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
1112
import { cookies } from "next/headers";
1213
import { redirect } from "next/navigation";
13-
import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
14-
import { getProject } from "@/lib/projects";
15-
import { checkApiKeySet } from "@/lib/api-keys";
14+
import { generators } from "openid-client";
15+
import * as yup from "yup";
1616

1717
const outerOAuthFlowExpirationInMinutes = 10;
1818

@@ -53,14 +53,14 @@ export const GET = createSmartRouteHandler({
5353
const project = await getProject(query.client_id);
5454

5555
if (!project) {
56-
throw new KnownErrors.ProjectNotFound();
56+
throw new KnownErrors.InvalidOAuthClientIdOrSecret(query.client_id);
5757
}
5858

5959
if (!await checkApiKeySet(query.client_id, { publishableClientKey: query.client_secret })) {
6060
throw new KnownErrors.ApiKeyNotFound();
6161
}
6262

63-
const provider = project.evaluatedConfig.oauthProviders.find((p) => p.id === params.provider);
63+
const provider = project.config.oauth_providers.find((p) => p.id === params.provider);
6464
if (!provider || !provider.enabled) {
6565
throw new KnownErrors.OAuthProviderNotFoundOrNotEnabled();
6666
}
@@ -89,7 +89,7 @@ export const GET = createSmartRouteHandler({
8989
extraScope: query.provider_scope,
9090
});
9191

92-
const outerInfo = await prismaClient.oAuthOuterInfo.create({
92+
await prismaClient.oAuthOuterInfo.create({
9393
data: {
9494
innerState,
9595
info: {
@@ -115,7 +115,7 @@ export const GET = createSmartRouteHandler({
115115
// prevent CSRF by keeping track of the inner state in cookies
116116
// the callback route must ensure that the inner state cookie is set
117117
cookies().set(
118-
"stack-oauth-inner-state-" + innerState,
118+
"stack-oauth-" + innerState,
119119
"true",
120120
{
121121
httpOnly: true,

apps/backend/src/app/api/v1/auth/oauth/callback/[provider]/route.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
1-
import * as yup from "yup";
1+
import { usersCrudHandlers } from "@/app/api/v1/users/crud";
2+
import { getProject } from "@/lib/projects";
3+
import { validateRedirectUrl } from "@/lib/redirect-urls";
4+
import { oauthCookieSchema } from "@/lib/tokens";
5+
import { getProvider, oauthServer } from "@/oauth";
26
import { prismaClient } from "@/prisma-client";
37
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
48
import { InvalidClientError, Request as OAuthRequest, Response as OAuthResponse } from "@node-oauth/oauth2-server";
5-
import { sendEmailFromTemplate } from "@/lib/emails";
9+
import { KnownError, KnownErrors } from "@stackframe/stack-shared";
10+
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
11+
import { yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
612
import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors";
7-
import { KnownError, KnownErrors, ProjectJson } from "@stackframe/stack-shared";
8-
import { yupObject, yupString, yupNumber, yupBoolean, yupArray, yupMixed } from "@stackframe/stack-shared/dist/schema-fields";
9-
import { sharedProviders } from "@stackframe/stack-shared/dist/interface/clientInterface";
10-
import { generators } from "openid-client";
11-
import { getProvider, oauthServer } from "@/oauth";
12-
import { decodeAccessToken, oauthCookieSchema } from "@/lib/tokens";
13+
import { extractScopes } from "@stackframe/stack-shared/dist/utils/strings";
1314
import { cookies } from "next/headers";
1415
import { redirect } from "next/navigation";
15-
import { getProject } from "@/lib/projects";
16-
import { validateRedirectUrl } from "@/lib/redirect-urls";
17-
import { extractScopes } from "@stackframe/stack-shared/dist/utils/strings";
18-
import { usersCrudHandlers } from "@/app/api/v1/users/crud";
1916

20-
const redirectOrThrowError = (error: KnownError, project: ProjectJson, errorRedirectUrl?: string) => {
21-
if (!errorRedirectUrl || !validateRedirectUrl(errorRedirectUrl, project.evaluatedConfig.domains, project.evaluatedConfig.allowLocalhost)) {
17+
const redirectOrThrowError = (error: KnownError, project: ProjectsCrud["Admin"]["Read"], errorRedirectUrl?: string) => {
18+
if (!errorRedirectUrl || !validateRedirectUrl(errorRedirectUrl, project.config.domains, project.config.allow_localhost)) {
2219
throw error;
2320
}
2421

@@ -89,7 +86,7 @@ export const GET = createSmartRouteHandler({
8986
redirectOrThrowError(new KnownErrors.OuterOAuthTimeout(), project, errorRedirectUrl);
9087
}
9188

92-
const provider = project.evaluatedConfig.oauthProviders.find((p) => p.id === params.provider);
89+
const provider = project.config.oauth_providers.find((p) => p.id === params.provider);
9390
if (!provider || !provider.enabled) {
9491
throw new KnownErrors.OAuthProviderNotFoundOrNotEnabled();
9592
}
@@ -203,7 +200,7 @@ export const GET = createSmartRouteHandler({
203200
providerConfig: {
204201
connect: {
205202
projectConfigId_id: {
206-
projectConfigId: project.evaluatedConfig.id,
203+
projectConfigId: project.config.id,
207204
id: provider.id,
208205
},
209206
},
@@ -251,7 +248,7 @@ export const GET = createSmartRouteHandler({
251248
primary_email_verified: false, // TODO: check if email is verified with the provider
252249
primary_email_auth_enabled: false,
253250
oauth_providers: [{
254-
provider_id: provider.id,
251+
id: provider.id,
255252
account_id: userInfo.accountId,
256253
email: userInfo.email,
257254
}],

apps/backend/src/app/api/v1/auth/oauth/connected-accounts/access-token/[provider]/route.tsx renamed to apps/backend/src/app/api/v1/auth/oauth/connected-accounts/[provider]/access-token/route.tsx

File renamed without changes.

apps/backend/src/app/api/v1/auth/oauth/token/route.tsx

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,51 +10,42 @@ export const POST = createSmartRouteHandler({
1010
description: "This endpoint is used to exchange an authorization code or refresh token for an access token.",
1111
tags: ["Oauth"]
1212
},
13-
request: yupObject({
14-
body: yupObject({
15-
grant_type: yupString().oneOf(["refresh_token", "authorization_code"]).required(),
16-
code: yupString(),
17-
code_verifier: yupString(),
18-
redirect_uri: yupString(),
19-
refresh_token: yupString(),
20-
}).required(),
21-
}),
13+
request: yupObject({}),
2214
response: yupObject({
2315
statusCode: yupNumber().required(),
2416
bodyType: yupString().oneOf(["json"]).required(),
2517
body: yupMixed().required(),
2618
headers: yupMixed().required(),
2719
}),
28-
async handler({ body }, fullReq) {
29-
if (body.redirect_uri) {
30-
body.redirect_uri = body.redirect_uri.split('#')[0]; // remove hash
31-
}
32-
20+
async handler({}, fullReq) {
3321
const oauthRequest = new OAuthRequest({
34-
headers: fullReq.headers,
35-
query: Object.fromEntries(new URL(fullReq.url).searchParams.entries()),
36-
method: "POST",
37-
body: body,
22+
headers: {
23+
...fullReq.headers,
24+
"content-type": "application/x-www-form-urlencoded",
25+
},
26+
method: fullReq.method,
27+
body: fullReq.body,
28+
query: fullReq.query,
3829
});
3930

4031

4132
const oauthResponse = new OAuthResponse();
4233
try {
4334
await oauthServer.token(
44-
oauthRequest,
45-
oauthResponse,
46-
{
47-
// note the `accessTokenLifetime` won't have any effect here because we set it in the `generateAccessToken` function
48-
refreshTokenLifetime: 60 * 60 * 24 * 365, // 1 year
49-
alwaysIssueNewRefreshToken: false, // add token rotation later
50-
}
51-
);
35+
oauthRequest,
36+
oauthResponse,
37+
{
38+
// note the `accessTokenLifetime` won't have any effect here because we set it in the `generateAccessToken` function
39+
refreshTokenLifetime: 60 * 60 * 24 * 365, // 1 year
40+
alwaysIssueNewRefreshToken: false, // add token rotation later
41+
}
42+
);
5243
} catch (e) {
5344
if (e instanceof InvalidGrantError) {
54-
throw new KnownErrors.RefreshTokenExpired();
45+
throw new KnownErrors.RefreshTokenNotFoundOrExpired();
5546
}
5647
if (e instanceof InvalidClientError) {
57-
throw new KnownErrors.ProjectNotFound();
48+
throw new KnownErrors.InvalidOAuthClientIdOrSecret();
5849
}
5950
throw e;
6051
}

apps/backend/src/app/api/v1/auth/otp/send-sign-in-code/route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const POST = createSmartRouteHandler({
2727
bodyType: yupString().oneOf(["success"]).required(),
2828
}),
2929
async handler({ auth: { project }, body: { email, callback_url: callbackUrl } }, fullReq) {
30-
if (!project.evaluatedConfig.magicLinkEnabled) {
30+
if (!project.config.magic_link_enabled) {
3131
throw new StatusError(StatusError.Forbidden, "Magic link is not enabled for this project");
3232
}
3333

@@ -78,7 +78,7 @@ export const POST = createSmartRouteHandler({
7878
// TODO fill user object instead of specifying the extra variables below manually (sIVCH.sendCode would do this already)
7979
user: null,
8080
email,
81-
templateId: "MAGIC_LINK",
81+
templateType: "magic_link",
8282
extraVariables: {
8383
userDisplayName: userObj.displayName,
8484
userPrimaryEmail: userObj.primaryEmail,

0 commit comments

Comments
 (0)