2026-02-14 02:20:19 +00:00

278 lines
8.3 KiB
TypeScript

// routes/auth/validate.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 * as Iron from "iron";
import {
getUserTokens,
setStateKey,
setUserTokens,
} from "../../services/userService.ts";
import {
generatePassword,
IdTokenPayload,
isSecureRequest,
persistWithEncryptedCookie,
SessionCookie,
} from "./callback.ts";
import { sqlGetAllStateKeys } from "../../db/sqlUserRepository.ts";
export const handler = define.handlers({
async POST(ctx) {
try {
console.log("validate POST - validating tokens");
const redirectHome = Deno.env.get("REDIRECT_HOME") || "";
// Parse body - support both JSON and form data
let body: {
session?: string;
xx_state_id?: string;
caller_url?: string;
};
try {
const contentType = ctx.req.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
// Parse as JSON
body = await ctx.req.json();
} else if (
contentType.includes("application/x-www-form-urlencoded") ||
contentType.includes("multipart/form-data")
) {
// Parse as form data
const formData = await ctx.req.formData();
body = {
session: formData.get("session")?.toString(),
xx_state_id: formData.get("xx_state_id")?.toString(),
caller_url: formData.get("caller_url")?.toString(),
};
} else {
// Try JSON as fallback
body = await ctx.req.json();
}
} catch (_e) {
console.error("Failed to parse request body:", _e);
return new Response(null, {
status: 302,
headers: {
"Location": "/",
},
});
}
console.log("validate POST - body:", body);
const xxStateId = body.xx_state_id;
const sessionCookie = body.session;
const timestampNow = Date.now();
let sessionData: SessionCookie | null = null;
let theStateKey: string | null = null;
if (xxStateId && sessionCookie) {
// Use only SQL database for state keys (no KV store)
const allStateKeys = await sqlGetAllStateKeys();
console.log("validate POST - stateKeys from SQL:", allStateKeys);
const stateKeys = allStateKeys.filter((k) => k.state_id === xxStateId);
stateKeys.sort((a: { created_on: number }, b: { created_on: number }) =>
b.created_on - a.created_on
);
const _timestamp = timestampNow - 18000000;
console.log("validate POST - filtered stateKeys:", stateKeys);
for (const stateKey of stateKeys) {
let stateData: SessionCookie | null = null;
try {
stateData = await Iron.unseal(
sessionCookie,
stateKey.seal_key,
Iron.defaults,
) as SessionCookie;
} catch (_e) {
console.log("error unsealing cookie");
}
if (stateData && stateData.user_id) {
const thisHash = await hashText(JSON.stringify(stateData));
if (thisHash === stateKey.cookie_hash) {
const renewWindow = timestampNow - 300 * 1000;
const newKey = generatePassword();
if (stateKey.created_on < renewWindow) {
stateData.started_on = timestampNow;
const cookieHash = await hashText(JSON.stringify(stateData));
await setStateKey(newKey, xxStateId, cookieHash);
theStateKey = newKey;
} else {
theStateKey = stateKey.seal_key;
}
sessionData = stateData;
break;
}
}
}
}
console.log("validate POST - sessionData:", sessionData);
const userId = sessionData?.user_id;
const callerUrl = body.caller_url || redirectHome;
// Get user tokens - returns array for multiple sessions
const userTokensArray = userId ? await getUserTokens(userId) : null;
// Find the token for the current session (matching stateId)
const userTokens = userTokensArray?.find((t) => t.state_id === xxStateId);
if (!xxStateId || !sessionCookie || !userId || !userTokens) {
console.error("Missing refresh_token or invalid session");
return new Response(null, {
status: 302,
headers: {
"Location": "/",
},
});
}
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 tokens = {
access_token: "",
id_token: "",
refresh_token: "",
};
// Make refresh token request to validate and get new tokens
const params = new URLSearchParams();
params.append("client_id", clientId);
params.append("client_secret", clientSecret);
params.append("grant_type", "refresh_token");
params.append("refresh_token", userTokens.refresh_token);
params.append(
"scope",
`openid profile offline_access https://graph.microsoft.com/.default`,
);
const tokenResponse = await fetch(
`${tokenEndpoint}?nonce=${nonce}`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params,
},
);
if (!tokenResponse.ok) {
console.error(
"Failed to refresh token:",
tokenResponse.status,
await tokenResponse.text(),
);
return new Response(null, {
status: 302,
headers: {
"Location": "/",
},
});
}
const tokenData = await tokenResponse.json();
tokens.access_token = tokenData.access_token;
tokens.id_token = tokenData.id_token;
tokens.refresh_token = tokenData.refresh_token;
const response = new Response(null, {
status: 302,
headers: {
"Location": callerUrl,
"nonce": nonce,
},
});
// Decode id_token to extract session data
try {
const [_header, payload, _signature] = decode(
tokens.id_token,
);
if (payload) {
const p = payload as unknown as IdTokenPayload;
const roles = Array.isArray(p.roles) && p.roles.length > 0
? p.roles.join(",")
: "";
sessionData = {
started_on: Date.now(),
user_name: p.name || p.preferred_username || p.email || "",
user_id: p.oid || p.sub || "",
user_encrypted: false,
role: roles,
user_type: "tenant", // Default user type for validation
};
const newKey = generatePassword();
const cookieHash = await hashText(JSON.stringify(sessionData));
const success = await setStateKey(newKey, xxStateId, cookieHash);
theStateKey = newKey;
if (!success) {
console.error("Failed to set state key");
return new Response(null, {
status: 302,
headers: {
"Location": "/",
},
});
}
}
} catch (_e) {
console.error("Failed to decode id_token:", _e);
return new Response(null, {
status: 302,
headers: {
"Location": "/",
},
});
}
setUserTokens(
userId,
xxStateId,
tokens.access_token,
tokens.refresh_token,
);
setCookie(response.headers, {
name: "xx_state_id",
value: xxStateId,
httpOnly: true,
secure: isSecureRequest(ctx.req),
sameSite: "Lax",
path: "/",
});
return theStateKey && sessionData
? persistWithEncryptedCookie(
sessionData as SessionCookie,
theStateKey,
response,
ctx.req,
xxStateId,
)
: response;
} catch (e) {
console.error("Validation error:", e);
return new Response(null, {
status: 302,
headers: {
"Location": "/",
},
});
}
},
});