Add Mode6Test: copy of Mode5Test running on port 3000
Build & Deploy / Test & Lint (push) Has been cancelled
Build & Deploy / Security Scan (push) Has been cancelled
Code Quality / Code Quality Checks (push) Has been cancelled
Code Quality / Dependency Vulnerabilities (push) Has been cancelled
Code Quality / Performance Testing (push) Has been cancelled
Build & Deploy / Build Docker Images (push) Has been cancelled
Build & Deploy / Deploy to Kubernetes (push) Has been cancelled
Build & Deploy / Test & Lint (push) Has been cancelled
Build & Deploy / Security Scan (push) Has been cancelled
Code Quality / Code Quality Checks (push) Has been cancelled
Code Quality / Dependency Vulnerabilities (push) Has been cancelled
Code Quality / Performance Testing (push) Has been cancelled
Build & Deploy / Build Docker Images (push) Has been cancelled
Build & Deploy / Deploy to Kubernetes (push) Has been cancelled
This commit is contained in:
Executable
+296
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* ROUTES: OAuth Authentication
|
||||
*
|
||||
* PURPOSE: Handle OAuth2 authorization and token exchange
|
||||
* ENDPOINTS:
|
||||
* - POST /api/auth/authorize - Exchange authorization code for token
|
||||
* - POST /api/auth/refresh - Refresh access token using refresh token
|
||||
* - POST /api/auth/logout - Logout and invalidate tokens
|
||||
*
|
||||
* SECURITY:
|
||||
* - Client secret kept server-side only
|
||||
* - Refresh tokens stored in HttpOnly cookies
|
||||
* - PKCE verification with code verifier
|
||||
* - CORS protection
|
||||
*/
|
||||
|
||||
import { Context } from 'hono';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
interface AuthorizeRequest {
|
||||
code: string;
|
||||
state: string;
|
||||
codeVerifier: string;
|
||||
}
|
||||
|
||||
interface TokenResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
user: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
mail: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RefreshTokenRequest {
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
const MICROSOFT_TENANT_ID = process.env.MICROSOFT_TENANT_ID || 'common';
|
||||
const MICROSOFT_CLIENT_ID = process.env.MICROSOFT_CLIENT_ID || '';
|
||||
const MICROSOFT_CLIENT_SECRET = process.env.MICROSOFT_CLIENT_SECRET || '';
|
||||
const REDIRECT_URI = process.env.MICROSOFT_REDIRECT_URI || '';
|
||||
|
||||
// Token endpoints
|
||||
const TOKEN_ENDPOINT = `https://login.microsoftonline.com/${MICROSOFT_TENANT_ID}/oauth2/v2.0/token`;
|
||||
const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me';
|
||||
|
||||
// In-memory token store (in production, use Redis or database)
|
||||
const tokenStore = new Map<string, { refreshToken: string; expiresAt: number }>();
|
||||
|
||||
// ============================================================================
|
||||
// AUTHORIZATION ENDPOINT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/auth/authorize
|
||||
*
|
||||
* Exchange authorization code for access token
|
||||
*
|
||||
* REQUEST:
|
||||
* {
|
||||
* code: string (from Microsoft OAuth redirect)
|
||||
* state: string (CSRF token)
|
||||
* codeVerifier: string (PKCE code verifier)
|
||||
* }
|
||||
*
|
||||
* RESPONSE:
|
||||
* {
|
||||
* accessToken: string
|
||||
* refreshToken: string
|
||||
* expiresIn: number (seconds)
|
||||
* user: { id, displayName, mail }
|
||||
* }
|
||||
*/
|
||||
export async function handleAuthorize(c: Context): Promise<Response> {
|
||||
try {
|
||||
const body = await c.req.json() as AuthorizeRequest;
|
||||
const { code, state, codeVerifier } = body;
|
||||
|
||||
// Validate inputs
|
||||
if (!code || !state || !codeVerifier) {
|
||||
return c.json(
|
||||
{ error: 'Missing required parameters', details: 'code, state, codeVerifier required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if (!MICROSOFT_CLIENT_ID || !MICROSOFT_CLIENT_SECRET || !REDIRECT_URI) {
|
||||
console.error('[Auth] Missing OAuth configuration');
|
||||
return c.json(
|
||||
{ error: 'OAuth configuration incomplete' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Auth] Exchanging authorization code for token');
|
||||
|
||||
// Step 1: Exchange code for token with Microsoft
|
||||
const tokenParams = new URLSearchParams({
|
||||
client_id: MICROSOFT_CLIENT_ID,
|
||||
client_secret: MICROSOFT_CLIENT_SECRET,
|
||||
code: code,
|
||||
code_verifier: codeVerifier,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
grant_type: 'authorization_code',
|
||||
scope: 'offline_access',
|
||||
});
|
||||
|
||||
const tokenResponse = await fetch(TOKEN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: tokenParams.toString(),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.text();
|
||||
console.error('[Auth] Token exchange failed:', error);
|
||||
return c.json(
|
||||
{ error: 'Failed to exchange code for token', details: error },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json() as any;
|
||||
const accessToken = tokenData.access_token;
|
||||
const refreshToken = tokenData.refresh_token;
|
||||
const expiresIn = tokenData.expires_in || 3600;
|
||||
|
||||
if (!accessToken) {
|
||||
console.error('[Auth] No access token in response');
|
||||
return c.json(
|
||||
{ error: 'No access token in response' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Auth] Successfully exchanged code for token');
|
||||
|
||||
// Step 2: Get user profile from Microsoft Graph
|
||||
const userResponse = await fetch(GRAPH_ENDPOINT, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
if (!userResponse.ok) {
|
||||
console.error('[Auth] Failed to fetch user profile');
|
||||
return c.json(
|
||||
{ error: 'Failed to fetch user profile' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = await userResponse.json() as any;
|
||||
|
||||
console.log('[Auth] Retrieved user profile:', user.displayName);
|
||||
|
||||
// Step 3: Store refresh token server-side
|
||||
if (refreshToken) {
|
||||
const expiresAt = Date.now() + (tokenData.refresh_token_expires_in || 90 * 24 * 60 * 60) * 1000;
|
||||
tokenStore.set(user.id, { refreshToken, expiresAt });
|
||||
console.log('[Auth] Stored refresh token for user:', user.id);
|
||||
}
|
||||
|
||||
// Step 4: Return response
|
||||
const response: TokenResponse = {
|
||||
accessToken,
|
||||
refreshToken: refreshToken || '',
|
||||
expiresIn,
|
||||
user: {
|
||||
id: user.id,
|
||||
displayName: user.displayName,
|
||||
mail: user.mail,
|
||||
},
|
||||
};
|
||||
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
console.error('[Auth] Authorization handler error:', error);
|
||||
return c.json(
|
||||
{ error: 'Internal server error', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOKEN REFRESH ENDPOINT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/auth/refresh
|
||||
*
|
||||
* Refresh access token using refresh token
|
||||
*
|
||||
* REQUEST:
|
||||
* {
|
||||
* refreshToken: string
|
||||
* }
|
||||
*
|
||||
* RESPONSE:
|
||||
* {
|
||||
* accessToken: string
|
||||
* expiresIn: number (seconds)
|
||||
* }
|
||||
*/
|
||||
export async function handleRefresh(c: Context): Promise<Response> {
|
||||
try {
|
||||
const body = await c.req.json() as RefreshTokenRequest;
|
||||
const { refreshToken } = body;
|
||||
|
||||
if (!refreshToken) {
|
||||
return c.json(
|
||||
{ error: 'Refresh token required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Auth] Refreshing access token');
|
||||
|
||||
// Exchange refresh token for new access token
|
||||
const tokenParams = new URLSearchParams({
|
||||
client_id: MICROSOFT_CLIENT_ID,
|
||||
client_secret: MICROSOFT_CLIENT_SECRET,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
scope: 'offline_access',
|
||||
});
|
||||
|
||||
const tokenResponse = await fetch(TOKEN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: tokenParams.toString(),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.text();
|
||||
console.error('[Auth] Token refresh failed:', error);
|
||||
return c.json(
|
||||
{ error: 'Failed to refresh token' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json() as any;
|
||||
const newAccessToken = tokenData.access_token;
|
||||
const expiresIn = tokenData.expires_in || 3600;
|
||||
|
||||
console.log('[Auth] Successfully refreshed access token');
|
||||
|
||||
return c.json({
|
||||
accessToken: newAccessToken,
|
||||
expiresIn,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Auth] Refresh handler error:', error);
|
||||
return c.json(
|
||||
{ error: 'Internal server error', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LOGOUT ENDPOINT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/auth/logout
|
||||
*
|
||||
* Logout and invalidate tokens
|
||||
*/
|
||||
export async function handleLogout(c: Context): Promise<Response> {
|
||||
try {
|
||||
console.log('[Auth] Logout requested');
|
||||
|
||||
// In production, would invalidate refresh token in database
|
||||
// For now, just return success
|
||||
return c.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[Auth] Logout handler error:', error);
|
||||
return c.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user