// routes/auth/callback.ts import { setCookie } from "$std/http/cookie.ts"; import { decode } from "djwt"; import { generateNonce, hashText } from "../../services/stateMiddleware.ts"; import { define } from "../../utils.ts"; import { createUser, getUser, setStateKey, setUserTokens, } from "../../services/userService.ts"; import * as Iron from "iron"; // Azure AD (CIAM/B2C) id_token payload shape export interface IdTokenPayload { aud: string; iss: string; iat: number; nbf: number; exp: number; aio?: string; email?: string; family_name?: string; given_name?: string; idp?: string; name?: string; nonce?: string; oid?: string; preferred_username?: string; rh?: string; roles?: string[]; sid?: string; sub?: string; tid?: string; uti?: string; ver?: string; [key: string]: unknown; } export function generatePassword(length = 32) { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?"; const passwordArray = new Uint8Array(length); crypto.getRandomValues(passwordArray); return Array.from(passwordArray) .map((x) => charset[x % charset.length]) .join(""); } export function isSecureRequest(req: Request): boolean { // Check for standard HTTPS if (req.url.startsWith("https://")) { return true; } // Check for common reverse proxy headers const forwardedProto = req.headers.get("x-forwarded-proto"); const forwardedSsl = req.headers.get("x-forwarded-ssl"); const frontEndHttps = req.headers.get("front-end-https"); const xArrSsl = req.headers.get("x-arr-ssl"); // IIS specific return forwardedProto === "https" || forwardedSsl === "on" || frontEndHttps === "on" || xArrSsl === "true"; } export interface SessionCookie { started_on: number; user_name: string; user_encrypted: boolean; user_id: string; role: string; } export async function persistWithEncryptedCookie( session: SessionCookie, stateKey: string, response: Response, req: Request, xxStateId: string, ) { const encryptedData = await Iron.seal( session, stateKey, Iron.defaults, ); try { setCookie(response.headers, { name: "xx_state_id", value: xxStateId, httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000), secure: req ? isSecureRequest(req) : true, sameSite: "Lax", path: "/", }); setCookie(response.headers, { name: "session", value: encryptedData, expires: new Date(Date.now() + 24 * 60 * 60 * 1000), httpOnly: true, secure: req ? isSecureRequest(req) : true, sameSite: "Lax", path: "/", }); } catch (_error) { // console.log("error:"); // console.log(JSON.stringify(error)); } return response; } export const handler = define.handlers(async ( ctx, ) => { try { const uri = new URL(ctx.req.url); const preXxStateId = uri.searchParams.get("xx-state-id"); const xxStateId = preXxStateId || crypto.randomUUID(); const codeParam: string | null = uri.searchParams.get("code"); const nonce = generateNonce(); const tokenEndpoint = Deno.env.get("TOKEN_URL") || ""; const clientId = Deno.env.get("CLIENT_ID") || ""; const clientSecret = Deno.env.get("CLIENT_SECRET") || ""; const redirectHome = Deno.env.get("REDIRECT_HOME") || ""; let theStateKey: string | null = null; let sessionData: SessionCookie | null = null; const tokens = { access_token: "", id_token: "", refresh_token: "", }; if (codeParam) { const params = new URLSearchParams(); params.append("client_id", clientId); params.append("client_secret", clientSecret); params.append("grant_type", "authorization_code"); params.append("code", codeParam); params.append("redirect_uri", redirectHome + "/auth/callback"); params.append( "scope", `openid profile offline_access https://graph.microsoft.com/.default`, ); const response = await fetch( `${tokenEndpoint}?nonce=${nonce}`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params, }, ); if (!response.ok) { console.error( "Failed to exchange code:", response.status, await response.text(), ); } else { const tokenResponse = await response.json(); tokens.access_token = tokenResponse.access_token; tokens.id_token = tokenResponse.id_token; tokens.refresh_token = tokenResponse.refresh_token; } } const response = new Response(null, { status: 302, headers: { "Location": `${redirectHome}`, "nonce": nonce, }, }); try { const [_header, payload, _signature] = decode( tokens.id_token, ); if (payload) { const p = payload as unknown as IdTokenPayload; const name = p.name || p.preferred_username || p.given_name + " " + p.family_name || p.email || ""; const roles = Array.isArray(p.roles) && p.roles.length > 0 ? p.roles.join(",") : ""; const userId = p.oid || p.sub || ""; const existingUser = await getUser(userId); sessionData = { started_on: Date.now(), user_name: existingUser?.name || name, user_encrypted: existingUser?.encrypted_data ? true : false, user_id: userId, role: roles, }; if (!existingUser) { // Create/update user before saving tokens (to satisfy foreign key constraint) await createUser( sessionData.user_id, sessionData.user_name, p.email || "", "", "", "", "", "", "", "", ); } await setUserTokens( sessionData.user_id, xxStateId, tokens.access_token, tokens.refresh_token, ); theStateKey = generatePassword(); const cookieHash = await hashText(JSON.stringify(sessionData)); const success = await setStateKey(theStateKey, xxStateId, cookieHash); if (!success) { console.error("Failed to set state key"); return new Response(null, { status: 302, headers: { "Location": "/", }, }); } } } catch (_e) { void 0; } return theStateKey && sessionData ? persistWithEncryptedCookie( sessionData as SessionCookie, theStateKey, response, ctx.req, xxStateId, ) : response; } catch (e) { console.error(e); return new Response(null, { status: 302, headers: { "Location": "/", }, }); } });