-
Notifications
You must be signed in to change notification settings - Fork 78
Expand file tree
/
Copy pathroute.ts
More file actions
140 lines (123 loc) · 3.78 KB
/
Copy pathroute.ts
File metadata and controls
140 lines (123 loc) · 3.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import {
acceptConsentRequest,
getServerSession,
rejectConsentRequest,
} from "@ory/nextjs/app"
import { cookies } from "next/headers"
import { NextResponse } from "next/server"
interface ConsentBody {
action?: string
consent_challenge?: string
grant_scope?: string | string[]
remember?: boolean | string
csrf_token?: string
}
async function parseRequest(request: Request): Promise<ConsentBody> {
const contentType = request.headers.get("content-type") || ""
if (contentType.includes("application/json")) {
return (await request.json()) as ConsentBody
}
if (
contentType.includes("application/x-www-form-urlencoded") ||
contentType.includes("multipart/form-data")
) {
const formData = await request.formData()
return {
action: formData.get("action") as string,
consent_challenge: formData.get("consent_challenge") as string,
grant_scope: formData.getAll("grant_scope") as string[],
remember: formData.get("remember") as string,
csrf_token: formData.get("csrf_token") as string,
}
}
// Try JSON as fallback
try {
return (await request.json()) as ConsentBody
} catch {
return {}
}
}
export async function POST(request: Request) {
// Security: Verify session exists before processing consent
const session = await getServerSession()
if (!session) {
console.error("Consent security: No session found")
return NextResponse.json(
{ error: "unauthorized", error_description: "No session" },
{ status: 401 },
)
}
const identityId = session.identity?.id
if (!identityId) {
console.error("Consent security: Session has no identity ID")
return NextResponse.json(
{ error: "unauthorized", error_description: "Invalid session" },
{ status: 401 },
)
}
const body = await parseRequest(request)
// Defense in depth: the CSRF middleware validates the signed cookie pair,
// but the token extraction of @csrf-armor/nextjs falls back to the cookie
// itself, so the submitted field must be compared against it explicitly.
const cookieStore = await cookies()
const csrfCookie = cookieStore.get("csrf-token")?.value
if (!csrfCookie || body.csrf_token !== csrfCookie) {
return NextResponse.json(
{ error: "invalid_request", error_description: "CSRF token mismatch" },
{ status: 403 },
)
}
const action = body.action
const consentChallenge = body.consent_challenge
const grantScope = Array.isArray(body.grant_scope)
? body.grant_scope
: body.grant_scope
? [body.grant_scope]
: []
const remember = body.remember === true || body.remember === "true"
if (!consentChallenge) {
return NextResponse.json(
{
error: "invalid_request",
error_description: "Missing consent_challenge",
},
{ status: 400 },
)
}
try {
let redirectTo: string
if (action === "accept") {
redirectTo = await acceptConsentRequest(consentChallenge, {
grantScope,
remember,
identityId,
})
} else {
redirectTo = await rejectConsentRequest(consentChallenge, {
identityId,
})
}
return NextResponse.json({ redirect_to: redirectTo })
} catch (error) {
console.error("Consent error:", error)
// Check for identity mismatch error
if (
error instanceof Error &&
error.message.includes("does not match consent request subject")
) {
return NextResponse.json(
{
error: "forbidden",
error_description: "Session does not match consent request subject",
},
{ status: 403 },
)
}
return NextResponse.json(
{ error: "server_error", error_description: "Failed to process consent" },
{ status: 500 },
)
}
}