update
This commit is contained in:
@@ -7,7 +7,8 @@ import {
|
|||||||
getUserInfo,
|
getUserInfo,
|
||||||
} from "../auth/oidc";
|
} from "../auth/oidc";
|
||||||
import { requireAuth, syncUser } from "../middleware/auth";
|
import { requireAuth, syncUser } from "../middleware/auth";
|
||||||
import type { AuthenticatedRequest } from "../types";
|
import type { AuthenticatedRequest, AuthenticatedUser } from "../types";
|
||||||
|
import type { AuthSession } from "../auth/oidc";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -21,6 +22,26 @@ async function ensureOIDC() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Short-lived store for native app OIDC sessions, keyed by state.
|
||||||
|
// Entries are cleaned up after 10 minutes regardless of use.
|
||||||
|
const nativeOidcSessions = new Map<string, { session: AuthSession; expiresAt: number }>();
|
||||||
|
const NATIVE_SESSION_TTL_MS = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
function storeNativeSession(session: AuthSession): void {
|
||||||
|
nativeOidcSessions.set(session.state, {
|
||||||
|
session,
|
||||||
|
expiresAt: Date.now() + NATIVE_SESSION_TTL_MS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function popNativeSession(state: string): AuthSession | null {
|
||||||
|
const entry = nativeOidcSessions.get(state);
|
||||||
|
if (!entry) return null;
|
||||||
|
nativeOidcSessions.delete(state);
|
||||||
|
if (Date.now() > entry.expiresAt) return null;
|
||||||
|
return entry.session;
|
||||||
|
}
|
||||||
|
|
||||||
// GET /auth/login - Initiate OIDC login flow
|
// GET /auth/login - Initiate OIDC login flow
|
||||||
router.get("/login", async (req, res) => {
|
router.get("/login", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -28,7 +49,15 @@ router.get("/login", async (req, res) => {
|
|||||||
|
|
||||||
const redirectUri = req.query.redirect_uri as string | undefined;
|
const redirectUri = req.query.redirect_uri as string | undefined;
|
||||||
const session = createAuthSession(redirectUri);
|
const session = createAuthSession(redirectUri);
|
||||||
|
|
||||||
|
if (redirectUri) {
|
||||||
|
// Native app flow: store session by state so /auth/token can retrieve it
|
||||||
|
// without relying on the browser cookie jar.
|
||||||
|
storeNativeSession(session);
|
||||||
|
} else {
|
||||||
|
// Web flow: store session in the cookie-backed server session as before.
|
||||||
req.session.oidc = session;
|
req.session.oidc = session;
|
||||||
|
}
|
||||||
|
|
||||||
const authorizationUrl = getAuthorizationUrl(session, redirectUri);
|
const authorizationUrl = getAuthorizationUrl(session, redirectUri);
|
||||||
res.redirect(authorizationUrl);
|
res.redirect(authorizationUrl);
|
||||||
@@ -38,7 +67,7 @@ router.get("/login", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /auth/callback - OIDC callback handler
|
// GET /auth/callback - OIDC callback handler (web frontend only)
|
||||||
router.get("/callback", async (req, res) => {
|
router.get("/callback", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await ensureOIDC();
|
await ensureOIDC();
|
||||||
@@ -89,9 +118,9 @@ router.get("/me", requireAuth, (req: AuthenticatedRequest, res) => {
|
|||||||
res.json(req.user);
|
res.json(req.user);
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /auth/token - Exchange authorization code for tokens (for native apps)
|
// POST /auth/token - Exchange authorization code for tokens (native app flow)
|
||||||
// The session cookie set during /auth/login is sent automatically by ASWebAuthenticationSession,
|
// Session state is retrieved from the in-memory store by state value, so no
|
||||||
// so req.session.oidc contains the original state/nonce for validation.
|
// session cookie is required from the native client.
|
||||||
router.post("/token", async (req, res) => {
|
router.post("/token", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await ensureOIDC();
|
await ensureOIDC();
|
||||||
@@ -103,20 +132,12 @@ router.post("/token", async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oidcSession = req.session.oidc;
|
const oidcSession = popNativeSession(state);
|
||||||
if (!oidcSession) {
|
if (!oidcSession) {
|
||||||
res.status(400).json({ error: "No active OIDC session. Initiate login first." });
|
res.status(400).json({ error: "OIDC session not found or expired. Initiate login again." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that the returned state matches what was stored in the session
|
|
||||||
if (oidcSession.state !== state) {
|
|
||||||
res.status(400).json({ error: "State mismatch" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the session's own codeVerifier (generated by the backend during /auth/login)
|
|
||||||
// rather than a client-supplied one, to prevent verifier substitution attacks.
|
|
||||||
const session = {
|
const session = {
|
||||||
codeVerifier: oidcSession.codeVerifier,
|
codeVerifier: oidcSession.codeVerifier,
|
||||||
state: oidcSession.state,
|
state: oidcSession.state,
|
||||||
@@ -129,9 +150,6 @@ router.post("/token", async (req, res) => {
|
|||||||
const user = await getUserInfo(tokenSet);
|
const user = await getUserInfo(tokenSet);
|
||||||
await syncUser(user);
|
await syncUser(user);
|
||||||
|
|
||||||
// Clear OIDC session state now that it has been consumed
|
|
||||||
delete req.session.oidc;
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
access_token: tokenSet.access_token,
|
access_token: tokenSet.access_token,
|
||||||
id_token: tokenSet.id_token,
|
id_token: tokenSet.id_token,
|
||||||
|
|||||||
Reference in New Issue
Block a user