fix
This commit is contained in:
@@ -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> {
|
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 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 id = String(userInfo.sub);
|
||||||
const username = String(userInfo.preferred_username || userInfo.name || id);
|
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');
|
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;
|
||||||
}
|
}
|
||||||
@@ -16,14 +16,17 @@ export async function requireAuth(
|
|||||||
|
|
||||||
// 2. Bearer token auth (iOS / native clients)
|
// 2. Bearer token auth (iOS / native clients)
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
|
console.log('[requireAuth] authorization header:', authHeader ? `${authHeader.slice(0, 20)}…` : '(none)');
|
||||||
if (authHeader?.startsWith('Bearer ')) {
|
if (authHeader?.startsWith('Bearer ')) {
|
||||||
const accessToken = authHeader.slice(7);
|
const accessToken = authHeader.slice(7);
|
||||||
try {
|
try {
|
||||||
const user = await verifyBearerToken(accessToken);
|
const user = await verifyBearerToken(accessToken);
|
||||||
req.user = user;
|
req.user = user;
|
||||||
return next();
|
return next();
|
||||||
} catch {
|
} catch (err) {
|
||||||
res.status(401).json({ error: 'Unauthorized' });
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error('[requireAuth] verifyBearerToken failed:', err);
|
||||||
|
res.status(401).json({ error: `Unauthorized: ${message}` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ extension Notification.Name {
|
|||||||
|
|
||||||
struct TokenResponse: Codable {
|
struct TokenResponse: Codable {
|
||||||
let accessToken: String
|
let accessToken: String
|
||||||
let idToken: String
|
let idToken: String?
|
||||||
let tokenType: String
|
let tokenType: String
|
||||||
let expiresIn: Int?
|
let expiresIn: Int?
|
||||||
let user: User
|
let user: User
|
||||||
|
|||||||
@@ -60,10 +60,11 @@ actor APIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if httpResponse.statusCode == 401 {
|
if httpResponse.statusCode == 401 {
|
||||||
|
let message = try? decoder.decode(ErrorResponse.self, from: data).error
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
AuthManager.shared.clearAuth()
|
AuthManager.shared.clearAuth()
|
||||||
}
|
}
|
||||||
throw NetworkError.unauthorized
|
throw NetworkError.httpError(statusCode: 401, message: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard (200...299).contains(httpResponse.statusCode) else {
|
guard (200...299).contains(httpResponse.statusCode) else {
|
||||||
@@ -131,10 +132,11 @@ actor APIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if httpResponse.statusCode == 401 {
|
if httpResponse.statusCode == 401 {
|
||||||
|
let message = try? decoder.decode(ErrorResponse.self, from: data).error
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
AuthManager.shared.clearAuth()
|
AuthManager.shared.clearAuth()
|
||||||
}
|
}
|
||||||
throw NetworkError.unauthorized
|
throw NetworkError.httpError(statusCode: 401, message: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard (200...299).contains(httpResponse.statusCode) else {
|
guard (200...299).contains(httpResponse.statusCode) else {
|
||||||
|
|||||||
@@ -9,22 +9,22 @@ enum APIEndpoint {
|
|||||||
static let me = "/auth/me"
|
static let me = "/auth/me"
|
||||||
|
|
||||||
// Clients
|
// Clients
|
||||||
static let clients = "/api/clients"
|
static let clients = "/clients"
|
||||||
static func client(id: String) -> String { "/api/clients/\(id)" }
|
static func client(id: String) -> String { "/clients/\(id)" }
|
||||||
|
|
||||||
// Projects
|
// Projects
|
||||||
static let projects = "/api/projects"
|
static let projects = "/projects"
|
||||||
static func project(id: String) -> String { "/api/projects/\(id)" }
|
static func project(id: String) -> String { "/projects/\(id)" }
|
||||||
|
|
||||||
// Time Entries
|
// Time Entries
|
||||||
static let timeEntries = "/api/time-entries"
|
static let timeEntries = "/time-entries"
|
||||||
static let timeEntriesStatistics = "/api/time-entries/statistics"
|
static let timeEntriesStatistics = "/time-entries/statistics"
|
||||||
static func timeEntry(id: String) -> String { "/api/time-entries/\(id)" }
|
static func timeEntry(id: String) -> String { "/time-entries/\(id)" }
|
||||||
|
|
||||||
// Timer
|
// Timer
|
||||||
static let timer = "/api/timer"
|
static let timer = "/timer"
|
||||||
static let timerStart = "/api/timer/start"
|
static let timerStart = "/timer/start"
|
||||||
static let timerStop = "/api/timer/stop"
|
static let timerStop = "/timer/stop"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct APIEndpoints {
|
struct APIEndpoints {
|
||||||
|
|||||||
Reference in New Issue
Block a user