151 lines
4.2 KiB
TypeScript
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();
|