This commit is contained in:
2026-02-18 22:58:41 +01:00
parent b3db7cbd7b
commit 1ca76b0fec
5 changed files with 41 additions and 17 deletions

View File

@@ -170,10 +170,27 @@ export async function verifyToken(tokenSet: TokenSet): Promise<boolean> {
}
}
// Cache userinfo responses to avoid hitting the OIDC provider on every request.
// Entries expire after 5 minutes. The cache is keyed by the raw access token.
const userinfoCache = new Map<string, { user: AuthenticatedUser; expiresAt: number }>();
const USERINFO_CACHE_TTL_MS = 5 * 60 * 1000;
export async function verifyBearerToken(accessToken: string): Promise<AuthenticatedUser> {
const cached = userinfoCache.get(accessToken);
if (cached && Date.now() < cached.expiresAt) {
return cached.user;
}
const client = getOIDCClient();
const userInfo = await client.userinfo(accessToken);
let userInfo: Awaited<ReturnType<typeof client.userinfo>>;
try {
userInfo = await client.userinfo(accessToken);
} catch (err) {
// Remove any stale cache entry for this token
userinfoCache.delete(accessToken);
throw err;
}
const id = String(userInfo.sub);
const username = String(userInfo.preferred_username || userInfo.name || id);
@@ -184,5 +201,7 @@ export async function verifyBearerToken(accessToken: string): Promise<Authentica
throw new Error('Email not provided by OIDC provider');
}
return { id, username, fullName, email };
const user: AuthenticatedUser = { id, username, fullName, email };
userinfoCache.set(accessToken, { user, expiresAt: Date.now() + USERINFO_CACHE_TTL_MS });
return user;
}

View File

@@ -16,14 +16,17 @@ export async function requireAuth(
// 2. Bearer token auth (iOS / native clients)
const authHeader = req.headers.authorization;
console.log('[requireAuth] authorization header:', authHeader ? `${authHeader.slice(0, 20)}` : '(none)');
if (authHeader?.startsWith('Bearer ')) {
const accessToken = authHeader.slice(7);
try {
const user = await verifyBearerToken(accessToken);
req.user = user;
return next();
} catch {
res.status(401).json({ error: 'Unauthorized' });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error('[requireAuth] verifyBearerToken failed:', err);
res.status(401).json({ error: `Unauthorized: ${message}` });
return;
}
}

View File

@@ -198,7 +198,7 @@ extension Notification.Name {
struct TokenResponse: Codable {
let accessToken: String
let idToken: String
let idToken: String?
let tokenType: String
let expiresIn: Int?
let user: User

View File

@@ -60,10 +60,11 @@ actor APIClient {
}
if httpResponse.statusCode == 401 {
let message = try? decoder.decode(ErrorResponse.self, from: data).error
await MainActor.run {
AuthManager.shared.clearAuth()
}
throw NetworkError.unauthorized
throw NetworkError.httpError(statusCode: 401, message: message)
}
guard (200...299).contains(httpResponse.statusCode) else {
@@ -131,10 +132,11 @@ actor APIClient {
}
if httpResponse.statusCode == 401 {
let message = try? decoder.decode(ErrorResponse.self, from: data).error
await MainActor.run {
AuthManager.shared.clearAuth()
}
throw NetworkError.unauthorized
throw NetworkError.httpError(statusCode: 401, message: message)
}
guard (200...299).contains(httpResponse.statusCode) else {

View File

@@ -9,22 +9,22 @@ enum APIEndpoint {
static let me = "/auth/me"
// Clients
static let clients = "/api/clients"
static func client(id: String) -> String { "/api/clients/\(id)" }
static let clients = "/clients"
static func client(id: String) -> String { "/clients/\(id)" }
// Projects
static let projects = "/api/projects"
static func project(id: String) -> String { "/api/projects/\(id)" }
static let projects = "/projects"
static func project(id: String) -> String { "/projects/\(id)" }
// Time Entries
static let timeEntries = "/api/time-entries"
static let timeEntriesStatistics = "/api/time-entries/statistics"
static func timeEntry(id: String) -> String { "/api/time-entries/\(id)" }
static let timeEntries = "/time-entries"
static let timeEntriesStatistics = "/time-entries/statistics"
static func timeEntry(id: String) -> String { "/time-entries/\(id)" }
// Timer
static let timer = "/api/timer"
static let timerStart = "/api/timer/start"
static let timerStop = "/api/timer/stop"
static let timer = "/timer"
static let timerStart = "/timer/start"
static let timerStop = "/timer/stop"
}
struct APIEndpoints {