This commit is contained in:
Matthias Hochmeister
2026-02-23 17:08:58 +01:00
commit f09748f4a1
97 changed files with 17729 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
import axios, { AxiosError } from 'axios';
import authentikConfig from '../config/authentik';
import logger from '../utils/logger';
import { TokenResponse, UserInfo } from '../types/auth.types';
class AuthentikService {
/**
* 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 (basic validation)
* Note: For production, use a proper JWT verification library like jose or jsonwebtoken
*/
verifyIdToken(idToken: string): any {
try {
// Split the token into parts
const parts = idToken.split('.');
if (parts.length !== 3) {
throw new Error('Invalid ID token format');
}
// Decode the payload (Base64URL)
const payload = JSON.parse(
Buffer.from(parts[1], 'base64url').toString('utf-8')
);
// Basic validation
if (!payload.sub || !payload.email) {
throw new Error('Invalid ID token payload');
}
// Check expiration
if (payload.exp && payload.exp * 1000 < Date.now()) {
throw new Error('ID token has expired');
}
// Check issuer
if (payload.iss && !payload.iss.includes(authentikConfig.issuer)) {
logger.warn('ID token issuer mismatch', {
expected: authentikConfig.issuer,
received: payload.iss,
});
}
logger.info('ID token verified successfully', {
sub: payload.sub,
email: payload.email,
});
return payload;
} catch (error) {
logger.error('Failed to verify ID token', { 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();