|
1 | 1 | /** |
2 | 2 | * HTML layout template |
3 | 3 | */ |
4 | | -import type { AuthRequest } from "@cloudflare/workers-oauth-provider"; |
5 | 4 | import { html } from "hono/html"; |
6 | 5 | import type { HtmlEscapedString } from "hono/utils/html"; |
7 | | -import type { PKCECodePair } from "../types"; |
8 | 6 |
|
9 | 7 | export const layout = (content: HtmlEscapedString | string, title: string) => html` |
10 | 8 | <!DOCTYPE html> |
@@ -83,6 +81,17 @@ export const layout = (content: HtmlEscapedString | string, title: string) => ht |
83 | 81 | text-decoration: underline; |
84 | 82 | } |
85 | 83 |
|
| 84 | + /* Preserve text-white for button-styled links */ |
| 85 | + .markdown a.bg-primary, |
| 86 | + .markdown a.bg-secondary { |
| 87 | + color: white !important; |
| 88 | + } |
| 89 | +
|
| 90 | + .markdown a.bg-primary:hover, |
| 91 | + .markdown a.bg-secondary:hover { |
| 92 | + text-decoration: none; |
| 93 | + } |
| 94 | +
|
86 | 95 | .markdown blockquote { |
87 | 96 | border-left: 4px solid #f39c12; |
88 | 97 | padding-left: 1rem; |
@@ -171,108 +180,90 @@ export const layout = (content: HtmlEscapedString | string, title: string) => ht |
171 | 180 | </html> |
172 | 181 | `; |
173 | 182 |
|
174 | | -export const parseApproveFormBody = async (body: { |
175 | | - [x: string]: string | File; |
176 | | -}) => { |
177 | | - const action = body.action as string; |
178 | | - const email = body.email as string; |
179 | | - const password = body.password as string; |
180 | | - let oauthReqInfo: AuthRequest | null = null; |
181 | | - try { |
182 | | - oauthReqInfo = JSON.parse(body.oauthReqInfo as string) as AuthRequest; |
183 | | - } catch (e) { |
184 | | - oauthReqInfo = null; |
185 | | - } |
186 | | - |
187 | | - return { action, oauthReqInfo, email, password }; |
188 | | -}; |
189 | | - |
190 | 183 | /** |
191 | | - * Generate a random string for PKCE and state |
192 | | - * @param length Length of the random string |
193 | | - * @returns A URL-safe random string |
| 184 | + * Create a manual redirect confirmation page |
| 185 | + * Per OAuth 2.0 Security Best Practices (RFC 6819 section 7.12.2), |
| 186 | + * untrusted redirect URIs should require manual user confirmation |
| 187 | + * @param redirectUri The full redirect URI for the actual redirect |
| 188 | + * @param displayUrl The origin to display to the user (cleaner than full URL) |
| 189 | + * @returns HTML string for the confirmation page |
194 | 190 | */ |
195 | | -export function generateRandomString(length: number): string { |
196 | | - const array = new Uint8Array(length); |
197 | | - crypto.getRandomValues(array); |
198 | | - return Array.from(array) |
199 | | - .map((b) => b.toString(16).padStart(2, "0")) |
200 | | - .join("") |
201 | | - .substring(0, length); |
202 | | -} |
| 191 | +export const manualRedirectPage = (redirectUri: string, displayUrl: string) => html` |
| 192 | + <div class="markdown max-w-2xl mx-auto"> |
| 193 | + <div class="bg-white rounded-lg shadow-md p-8 border-l-4 border-accent"> |
| 194 | + <div class="mb-6"> |
| 195 | + <svg |
| 196 | + class="w-16 h-16 mx-auto text-accent" |
| 197 | + fill="none" |
| 198 | + stroke="currentColor" |
| 199 | + viewBox="0 0 24 24" |
| 200 | + xmlns="http://www.w3.org/2000/svg" |
| 201 | + > |
| 202 | + <path |
| 203 | + stroke-linecap="round" |
| 204 | + stroke-linejoin="round" |
| 205 | + stroke-width="2" |
| 206 | + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" |
| 207 | + ></path> |
| 208 | + </svg> |
| 209 | + </div> |
203 | 210 |
|
204 | | -/** |
205 | | - * Create a code verifier and code challenge pair for PKCE |
206 | | - * @returns A code verifier and challenge pair |
207 | | - */ |
208 | | -export async function createPKCECodes(): Promise<PKCECodePair> { |
209 | | - // Generate code verifier (random string between 43-128 chars) |
210 | | - const codeVerifier = generateRandomString(64); |
| 211 | + <h1 class="text-center mb-4">Authentication successful</h1> |
211 | 212 |
|
212 | | - // Create code challenge using SHA-256 |
213 | | - const encoder = new TextEncoder(); |
214 | | - const data = encoder.encode(codeVerifier); |
215 | | - const digest = await crypto.subtle.digest("SHA-256", data); |
| 213 | + <div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6"> |
| 214 | + <div class="flex"> |
| 215 | + <div class="flex-shrink-0"> |
| 216 | + <svg |
| 217 | + class="h-5 w-5 text-yellow-400" |
| 218 | + viewBox="0 0 20 20" |
| 219 | + fill="currentColor" |
| 220 | + > |
| 221 | + <path |
| 222 | + fill-rule="evenodd" |
| 223 | + d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" |
| 224 | + clip-rule="evenodd" |
| 225 | + /> |
| 226 | + </svg> |
| 227 | + </div> |
| 228 | + <div class="ml-3"> |
| 229 | + <p class="text-sm text-yellow-700 font-medium"> |
| 230 | + Security notice |
| 231 | + </p> |
| 232 | + </div> |
| 233 | + </div> |
| 234 | + </div> |
216 | 235 |
|
217 | | - // Convert digest to base64url format |
218 | | - const base64Digest = btoa(String.fromCharCode(...new Uint8Array(digest))) |
219 | | - .replace(/\+/g, "-") |
220 | | - .replace(/\//g, "_") |
221 | | - .replace(/=/g, ""); |
| 236 | + <p class="mb-4"> |
| 237 | + Your authentication was successful. However, for security reasons, you |
| 238 | + need to manually complete the redirect to the following URL: |
| 239 | + </p> |
222 | 240 |
|
223 | | - return { |
224 | | - codeVerifier, |
225 | | - codeChallenge: base64Digest, |
226 | | - }; |
227 | | -} |
| 241 | + <div class="bg-gray-100 p-4 rounded-md mb-6 break-all"> |
| 242 | + <code class="text-sm text-gray-800">${displayUrl}</code> |
| 243 | + </div> |
228 | 244 |
|
229 | | -const STANDARD_PROTOCOLS = new Set([ |
230 | | - "http:", |
231 | | - "https:", |
232 | | - "ftp:", |
233 | | - "file:", |
234 | | - "mailto:", |
235 | | - "tel:", |
236 | | - "ws:", |
237 | | - "wss:", |
238 | | - "sms:", |
239 | | - "data:", |
240 | | - "blob:", |
241 | | - "about:", |
242 | | - "chrome:", |
243 | | - "opera:", |
244 | | - "edge:", |
245 | | - "safari:", |
246 | | - "javascript:", |
247 | | -]); |
| 245 | + <div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6"> |
| 246 | + <p class="text-sm text-blue-700"> |
| 247 | + <strong>Why do I need to click?</strong> As a security measure, this |
| 248 | + authorization server requires manual confirmation before redirecting |
| 249 | + to third-party websites. This helps protect you from potential |
| 250 | + phishing attacks. |
| 251 | + </p> |
| 252 | + </div> |
248 | 253 |
|
249 | | -/** |
250 | | - * Check if a URL is a deep link |
251 | | - * @param url The URL to check |
252 | | - * @returns |
253 | | - */ |
254 | | -export function isDeepLink(url: string): boolean { |
255 | | - try { |
256 | | - const parsedUrl = new URL(url); |
257 | | - const protocol = parsedUrl.protocol.toLowerCase(); |
258 | | - return !STANDARD_PROTOCOLS.has(protocol); |
259 | | - } catch (e) { |
260 | | - return false; |
261 | | - } |
262 | | -} |
| 254 | + <div class="text-center"> |
| 255 | + <a |
| 256 | + href="${redirectUri}" |
| 257 | + class="inline-block bg-primary hover:bg-primary/90 text-white font-semibold py-3 px-8 rounded-lg transition-colors duration-200 shadow-md hover:shadow-lg" |
| 258 | + > |
| 259 | + Click here to complete authentication |
| 260 | + </a> |
| 261 | + </div> |
263 | 262 |
|
264 | | -export function isExceptionHost(urlString: string): boolean { |
265 | | - try { |
266 | | - const url = new URL(urlString); |
267 | | - const exceptionHosts = new Set([ |
268 | | - "playground.ai.cloudflare.com", |
269 | | - "mcp.docker.com", |
270 | | - "mcptotal.io", |
271 | | - // add more exception hosts here if needed |
272 | | - ]); |
273 | | - return exceptionHosts.has(url.hostname); |
274 | | - } catch (err) { |
275 | | - // Invalid URL string |
276 | | - return false; |
277 | | - } |
278 | | -} |
| 263 | + <p class="text-sm text-gray-500 mt-6 text-center"> |
| 264 | + Only click the button above if you trust the destination and initiated |
| 265 | + this authentication request. |
| 266 | + </p> |
| 267 | + </div> |
| 268 | + </div> |
| 269 | +`; |
0 commit comments