-
Notifications
You must be signed in to change notification settings - Fork 0
fix: uses db scope #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,3 +47,4 @@ node_modules/ | |
| /blob-report/ | ||
| /playwright/.cache/ | ||
| /playwright/.auth/ | ||
| .env*.local | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||||||||||
| import { db } from "@/lib/db"; | ||||||||||||||||||||||
| import { oauthClients, oauthRefreshTokens, users } from "@/lib/db/schema"; | ||||||||||||||||||||||
| import { oauthClients, oauthConsents, oauthRefreshTokens, users } from "@/lib/db/schema"; | ||||||||||||||||||||||
| import { verifyPassword } from "@/lib/auth/password"; | ||||||||||||||||||||||
| import { getOAuthCode, deleteOAuthCode } from "@/lib/redis"; | ||||||||||||||||||||||
| import { verifyCodeChallenge } from "@/lib/oauth/pkce"; | ||||||||||||||||||||||
|
|
@@ -138,6 +138,15 @@ async function handleAuthorizationCodeGrant( | |||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Get granted scopes from database | ||||||||||||||||||||||
| const consent = await db.query.oauthConsents.findFirst({ | ||||||||||||||||||||||
| where: and( | ||||||||||||||||||||||
| eq(oauthConsents.userId, user.id), | ||||||||||||||||||||||
| eq(oauthConsents.clientId, client.id) | ||||||||||||||||||||||
| ), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| const grantedScopes: string[] = consent ? JSON.parse(consent.scopes) : []; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Generate tokens | ||||||||||||||||||||||
| const scopeString = codeData.scopes.join(" "); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -150,13 +159,13 @@ async function handleAuthorizationCodeGrant( | |||||||||||||||||||||
| const idToken = await signIdToken( | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| sub: user.id, | ||||||||||||||||||||||
| email: codeData.scopes.includes("email") ? user.email : undefined, | ||||||||||||||||||||||
| email_verified: codeData.scopes.includes("email") ? user.emailVerified ?? false : undefined, | ||||||||||||||||||||||
| name: codeData.scopes.includes("profile") ? user.displayName ?? undefined : undefined, | ||||||||||||||||||||||
| preferred_username: codeData.scopes.includes("profile") ? user.username ?? undefined : undefined, | ||||||||||||||||||||||
| picture: codeData.scopes.includes("profile") | ||||||||||||||||||||||
| ? `${process.env.OAUTH_ISSUER_URL}/api/avatar/${user.avatarSeed || user.id}` | ||||||||||||||||||||||
| : undefined, | ||||||||||||||||||||||
| // Email claims based on granted scopes from database | ||||||||||||||||||||||
| email: grantedScopes.includes("email") ? user.email : undefined, | ||||||||||||||||||||||
| email_verified: grantedScopes.includes("email") ? user.emailVerified ?? false : undefined, | ||||||||||||||||||||||
| // Always include profile claims | ||||||||||||||||||||||
| name: user.displayName ?? undefined, | ||||||||||||||||||||||
| preferred_username: user.username ?? undefined, | ||||||||||||||||||||||
| picture: `${process.env.OAUTH_ISSUER_URL}/api/avatar/${user.avatarSeed || user.id}`, | ||||||||||||||||||||||
|
Comment on lines
+165
to
+168
|
||||||||||||||||||||||
| // Always include profile claims | |
| name: user.displayName ?? undefined, | |
| preferred_username: user.username ?? undefined, | |
| picture: `${process.env.OAUTH_ISSUER_URL}/api/avatar/${user.avatarSeed || user.id}`, | |
| // Profile claims based on granted scopes from database | |
| name: grantedScopes.includes("profile") ? (user.displayName ?? undefined) : undefined, | |
| preferred_username: grantedScopes.includes("profile") ? (user.username ?? undefined) : undefined, | |
| picture: grantedScopes.includes("profile") | |
| ? `${process.env.OAUTH_ISSUER_URL}/api/avatar/${user.avatarSeed || user.id}` | |
| : undefined, |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,8 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { db } from "@/lib/db"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { users } from "@/lib/db/schema"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { oauthConsents, users } from "@/lib/db/schema"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { verifyAccessToken } from "@/lib/oauth/jwt"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { eq } from "drizzle-orm"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { and, eq } from "drizzle-orm"; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| async function handleUserInfo(request: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -40,19 +40,25 @@ async function handleUserInfo(request: NextRequest) { | |||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // Build response based on scopes | ||||||||||||||||||||||||||||||||||||||||||||
| const scopes = payload.scope.split(" "); | ||||||||||||||||||||||||||||||||||||||||||||
| // Get granted scopes from database | ||||||||||||||||||||||||||||||||||||||||||||
| const consent = await db.query.oauthConsents.findFirst({ | ||||||||||||||||||||||||||||||||||||||||||||
| where: and( | ||||||||||||||||||||||||||||||||||||||||||||
| eq(oauthConsents.userId, user.id), | ||||||||||||||||||||||||||||||||||||||||||||
| eq(oauthConsents.clientId, payload.client_id) | ||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| const grantedScopes: string[] = consent ? JSON.parse(consent.scopes) : []; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| const grantedScopes: string[] = consent ? JSON.parse(consent.scopes) : []; | |
| let grantedScopes: string[] = []; | |
| if (consent && consent.scopes) { | |
| try { | |
| const parsed = JSON.parse(consent.scopes); | |
| if (Array.isArray(parsed) && parsed.every((scope) => typeof scope === "string")) { | |
| grantedScopes = parsed; | |
| } else { | |
| console.warn("Invalid scopes format for consent", { | |
| userId: user.id, | |
| clientId: payload.client_id, | |
| }); | |
| } | |
| } catch (parseError) { | |
| console.warn("Failed to parse consent.scopes JSON", { | |
| error: parseError, | |
| userId: user.id, | |
| clientId: payload.client_id, | |
| }); | |
| } | |
| } |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Profile information is being returned unconditionally without checking if the user has granted the "profile" scope. According to OpenID Connect Core specification, profile claims (name, preferred_username, picture) should only be included when the "profile" scope has been granted. This could lead to unauthorized disclosure of user profile information.
The code should check if grantedScopes includes "profile" (or "read:profile" depending on your scope model) before adding these claims to the response, similar to how the email scope is being checked on line 61.
| // Build response - always include profile, email requires scope from DB | |
| const response: Record<string, unknown> = { | |
| sub: user.id, | |
| // Always include profile claims | |
| name: user.displayName, | |
| preferred_username: user.username, | |
| picture: `${process.env.OAUTH_ISSUER_URL}/api/avatar/${user.avatarSeed || user.id}`, | |
| }; | |
| // Build response - always include subject, other claims require appropriate scopes | |
| const response: Record<string, unknown> = { | |
| sub: user.id, | |
| }; | |
| // Include profile claims only if "profile" scope (or equivalent) was granted | |
| if (grantedScopes.includes("profile") || grantedScopes.includes("read:profile")) { | |
| response.name = user.displayName; | |
| response.preferred_username = user.username; | |
| response.picture = `${process.env.OAUTH_ISSUER_URL}/api/avatar/${user.avatarSeed || user.id}`; | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,7 +53,6 @@ describe("SPECIAL_SCOPES", () => { | |
| test("should contain expected special scopes", () => { | ||
| const keys = SPECIAL_SCOPES.map((s) => s.key); | ||
| expect(keys).toContain("openid"); | ||
| expect(keys).toContain("offline_access"); | ||
| }); | ||
|
Comment on lines
53
to
56
|
||
| }); | ||
|
|
||
|
|
@@ -65,7 +64,6 @@ describe("SCOPES (generated)", () => { | |
| test("should contain special scopes", () => { | ||
| const keys = SCOPES.map((s) => s.key); | ||
| expect(keys).toContain("openid"); | ||
| expect(keys).toContain("offline_access"); | ||
| }); | ||
|
|
||
| test("should contain resource scopes with operation:resource format", () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON.parse is called without error handling. If the consent.scopes field contains invalid JSON, this will throw an exception that will be caught by the outer try-catch and result in a generic "server_error" response. Consider adding validation or using a safer parsing approach with error handling to provide a more specific error message or handle corruption gracefully.