inital
This commit is contained in:
158
backend/src/services/authentik.service.ts
Normal file
158
backend/src/services/authentik.service.ts
Normal 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();
|
||||
Reference in New Issue
Block a user