import axios, { AxiosError } from 'axios'; import * as jose from 'jose'; import authentikConfig from '../config/authentik'; import logger from '../utils/logger'; import { TokenResponse, UserInfo } from '../types/auth.types'; class AuthentikService { private jwks: ReturnType | null = null; private getJwks(): ReturnType { if (!this.jwks) { // Derive JWKS URI from the issuer URL // Issuer: https://auth.example.com/application/o/myapp/ // JWKS: https://auth.example.com/application/o/myapp/jwks/ const jwksUri = new URL('jwks/', authentikConfig.issuer.endsWith('/') ? authentikConfig.issuer : authentikConfig.issuer + '/'); this.jwks = jose.createRemoteJWKSet(jwksUri); } return this.jwks; } /** * Exchange authorization code for access and ID tokens */ async exchangeCodeForTokens(code: string): Promise { try { const params = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: authentikConfig.redirectUri, client_id: authentikConfig.clientId, client_secret: authentikConfig.clientSecret, }); const response = await axios.post( authentikConfig.tokenEndpoint, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, } ); logger.info('Successfully exchanged code for tokens'); return response.data; } catch (error) { this.handleError(error, 'Failed to exchange code for tokens'); throw error; } } /** * Fetch user information from Authentik using access token */ async getUserInfo(accessToken: string): Promise { try { const response = await axios.get( authentikConfig.userInfoEndpoint, { headers: { Authorization: `Bearer ${accessToken}`, }, } ); logger.info('Successfully fetched user info', { sub: response.data.sub, email: response.data.email, }); return response.data; } catch (error) { this.handleError(error, 'Failed to fetch user info'); throw error; } } /** * Verify and decode ID token using JWKS */ async verifyIdToken(idToken: string): Promise { try { const { payload } = await jose.jwtVerify(idToken, this.getJwks(), { issuer: authentikConfig.issuer, }); if (!payload.sub || !payload.email) { throw new Error('Invalid ID token payload: missing sub or email'); } logger.info('ID token verified successfully via JWKS', { sub: payload.sub, email: payload.email, }); return payload; } catch (error) { logger.error('Failed to verify ID token via JWKS', { error }); throw new Error('Invalid ID token'); } } /** * Refresh access token using refresh token */ async refreshAccessToken(refreshToken: string): Promise { try { const params = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: authentikConfig.clientId, client_secret: authentikConfig.clientSecret, }); const response = await axios.post( authentikConfig.tokenEndpoint, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, } ); logger.info('Successfully refreshed access token'); return response.data; } catch (error) { this.handleError(error, 'Failed to refresh access token'); throw error; } } /** * Handle axios errors with detailed logging */ private handleError(error: unknown, message: string): void { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; logger.error(message, { status: axiosError.response?.status, statusText: axiosError.response?.statusText, data: axiosError.response?.data, message: axiosError.message, }); } else { logger.error(message, { error }); } } } export default new AuthentikService();