Files
dashboard/backend/src/services/authentik.service.ts
Matthias Hochmeister 3c9b7d3446 apply security audit
2026-03-11 13:51:01 +01:00

151 lines
4.2 KiB
TypeScript

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<typeof jose.createRemoteJWKSet> | null = null;
private getJwks(): ReturnType<typeof jose.createRemoteJWKSet> {
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<TokenResponse> {
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<TokenResponse>(
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<UserInfo> {
try {
const response = await axios.get<UserInfo>(
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<jose.JWTPayload> {
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<TokenResponse> {
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<TokenResponse>(
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();