diff --git a/.gitignore b/.gitignore index 546c174..01e074d 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ node_modules/ /blob-report/ /playwright/.cache/ /playwright/.auth/ +.env*.local diff --git a/app/api/oauth/token/route.ts b/app/api/oauth/token/route.ts index fb1f901..4fa216c 100644 --- a/app/api/oauth/token/route.ts +++ b/app/api/oauth/token/route.ts @@ -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}`, nonce: codeData.nonce, auth_time: Math.floor(Date.now() / 1000), }, diff --git a/app/api/oauth/userinfo/route.ts b/app/api/oauth/userinfo/route.ts index 01849f4..2b25b2e 100644 --- a/app/api/oauth/userinfo/route.ts +++ b/app/api/oauth/userinfo/route.ts @@ -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) : []; + + // Build response - always include profile, email requires scope from DB const response: Record = { 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}`, }; - if (scopes.includes("profile")) { - response.name = user.displayName; - response.preferred_username = user.username; - response.picture = `${process.env.OAUTH_ISSUER_URL}/api/avatar/${user.avatarSeed || user.id}`; - } - - if (scopes.includes("email")) { + if (grantedScopes.includes("email")) { response.email = user.email; response.email_verified = user.emailVerified; } diff --git a/lib/scopes.test.ts b/lib/scopes.test.ts index 8010c91..a8beccf 100644 --- a/lib/scopes.test.ts +++ b/lib/scopes.test.ts @@ -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"); }); }); @@ -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", () => {