modularized auth template need testing

This commit is contained in:
Kallesh B S 2025-06-20 18:00:48 +05:30
parent 7770642b80
commit 600a1f7d75
3 changed files with 214 additions and 99 deletions

View File

@ -10,12 +10,6 @@ import { LogoutInterceptor } from 'src/interceptors/logout.interceptor';
export class AuthController { export class AuthController {
constructor(private readonly authService: AuthService) { } constructor(private readonly authService: AuthService) { }
// @Post('/login')
// login(@Body() body: AuthLoginDTO) {
// return this.authService.login(body);
// }
@Post('login') @Post('login')
@HttpCode(200) @HttpCode(200)
async loginClient(@Body() body: AuthLoginDTO, @Res({ passthrough: true }) res: Response, @Req() req: Request) { async loginClient(@Body() body: AuthLoginDTO, @Res({ passthrough: true }) res: Response, @Req() req: Request) {

View File

@ -11,6 +11,15 @@ import { UnauthorizedException } from 'src/exceptions/unauthorized.exception';
import { ConflictException } from 'src/exceptions/conflict.exception'; import { ConflictException } from 'src/exceptions/conflict.exception';
import { BadRequestException } from 'src/exceptions/badRequest.exception'; import { BadRequestException } from 'src/exceptions/badRequest.exception';
interface DecodedToken {
email: string;
[key: string]: any;
}
interface IntrospectResult {
active: boolean;
}
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private keyCache: Record<string, string> = {}; private keyCache: Record<string, string> = {};
@ -121,19 +130,44 @@ export class AuthService {
} }
private async getPublicKey(kid: string): Promise<string> { private async getPublicKey(kid: string): Promise<string> {
try {
// Check if the key is already cached
if (this.keyCache[kid]) return this.keyCache[kid]; if (this.keyCache[kid]) return this.keyCache[kid];
// Fetch the JWKS (JSON Web Key Set)
const { data } = await axios.get(this.JWKS_URL); const { data } = await axios.get(this.JWKS_URL);
const key = data.keys.find((k) => k.kid === kid);
if (!key) throw new InternalServerErrorException('Key not found'); // Validate the response shape
const pem = jwkToPem(key); if (!data?.keys || !Array.isArray(data.keys)) {
this.keyCache[kid] = pem; console.log("Invalid JWKS response");
return pem; throw new UnauthorizedException('Authentication failed');
} }
async introspectToken(token: string): Promise<any> { // Find the key with the matching kid
const key = data.keys.find((k) => k.kid === kid);
if (!key) {
console.log("Authentication failed: Key not found");
throw new UnauthorizedException('Authentication failed');
}
console.log("from AuthService introspectToken .......................... end"); // Convert JWK to PEM format and cache it
const pem = jwkToPem(key);
this.keyCache[kid] = pem;
return pem;
} catch (error) {
if (error instanceof UnauthorizedException) {
throw error; // Let UnauthorizedException bubble up as-is
}
// Optionally log error details for debugging
console.error('Failed to retrieve or process JWKS:', error);
// Wrap other errors as InternalServerException
console.log('Failed to retrieve public key');
throw new InternalServerErrorException();
}
}
async introspectTokenX(token: string): Promise<any> {
let det: any = await jwt.decode(token); let det: any = await jwt.decode(token);
const { email } = det; const { email } = det;
@ -170,68 +204,98 @@ export class AuthService {
} }
} }
async decodeToken(token: string): Promise<any> { async introspectToken(token: string, publicKey: string): Promise<IntrospectResult> {
console.log("from AuthService decodeToken .......................... start"); try {
// Securely verify the token
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'], }) as DecodedToken;
const { email } = decoded;
if (!email) throw new UnauthorizedException('Token does not contain an email.');
// const uid = await this.getUserIdByEmail(email);
// if (!uid) throw new UnauthorizedException('No user ID found for email.');
// const userInfoUrl = `${this.KEYCLOAK_URL}/admin/realms/${this.KEYCLOAK_REALM}/users/${uid}/sessions`;
// const adminToken = await this.getAdminAccessToken();
// const response = await axios.get(userInfoUrl, {
// headers: {
// Authorization: `Bearer ${adminToken}`,
// },
// });
const sessionData = await this.getUserSession(email);
return { active: Array.isArray(sessionData) && sessionData.length > 0 };
} catch (error: any) {
console.log("Error in introspectToken:", error.message || error);
if (error instanceof UnauthorizedException || error instanceof jwt.TokenExpiredError || error instanceof jwt.JsonWebTokenError || error instanceof jwt.NotBeforeError) {
throw new UnauthorizedException('Authentication failed');
}
throw new InternalServerErrorException('Failed to introspect token');
}
}
async getUserSession(email) {
try {
const uid = await this.getUserIdByEmail(email);
if (!uid) throw new UnauthorizedException('No user ID found for email.');
const userInfoUrl = `${this.KEYCLOAK_URL}/admin/realms/${this.KEYCLOAK_REALM}/users/${uid}/sessions`;
const adminToken = await this.getAdminAccessToken();
const response = await axios.get(userInfoUrl, {
headers: {
Authorization: `Bearer ${adminToken}`,
},
});
const sessionData = response.data;
return sessionData
} catch (error) {
if (error instanceof UnauthorizedException) throw error
console.log("Error while getting User session.....");
throw new InternalServerErrorException()
}
}
async decodeToken(token: string): Promise<any> {
const decodedHeader = jwt.decode(token, { complete: true }); const decodedHeader = jwt.decode(token, { complete: true });
if (!decodedHeader || typeof decodedHeader !== 'object') { if (!decodedHeader || typeof decodedHeader !== 'object') {
throw new Error('Invalid token format'); throw new UnauthorizedException("Authentication failed")
} }
console.log("✅ token decoded from decodeToken");
const kid: any = decodedHeader.header.kid; const kid: any = decodedHeader.header.kid;
const publicKey = await this.getPublicKey(kid); const publicKey = await this.getPublicKey(kid);
console.log("✅ publicKey got from decodeToken"); const introspection = await this.introspectToken(token, publicKey);
const introspection = await this.introspectToken(token);
console.log("✅ introspection got from decodeToken");
if (!introspection.active) { if (!introspection.active) {
console.log("❌ introspection active from decodeToken"); console.log("Introspect failed for token");
console.log("from AuthService decodeToken .......................... end");
throw new UnauthorizedException("Authentication Failed") throw new UnauthorizedException("Authentication Failed")
} }
console.log("✅ introspection active from decodeToken");
const verified = jwt.verify(token, publicKey, { algorithms: ['RS256'] }); const verified = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
console.log("✅ token verification successufull from decodeToken");
console.log("from AuthService decodeToken .......................... end");
return verified; return verified;
} }
async getAdminAccessTokenx(): Promise<string> {
const data = new URLSearchParams();
data.append('grant_type', 'client_credentials');
data.append('client_id', this.CLIENT_ID);
data.append('client_secret', this.CLIENT_SECRET);
try {
const response: AxiosResponse = await axios.post(this.KEYCLOAK_URL, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
return response.data.access_token;
} catch (error) {
console.error('Error getting Keycloak access token:', error.response ? error.response.data : error.message);
throw new Error('Failed to obtain Keycloak access token');
}
}
async loginUser(username: string, password: string, req: Request): Promise<any> { async loginUser(username: string, password: string, req: Request): Promise<any> {
const accessToken = req.cookies['access_token']; const access_token = req.cookies?.access_token;
const refreshToken = req.cookies['refresh_token']; const refresh_token = req.cookies?.refresh_token;
// if (!accessToken && !refreshToken) {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('grant_type', 'password'); params.append('grant_type', 'password');
@ -242,27 +306,41 @@ export class AuthService {
// params.append('scope', 'openid'); // params.append('scope', 'openid');
try { try {
const userSession = await this.getUserSession(username)
if (access_token && refresh_token && userSession.length > 0) {
let tokens = await this.getTokenFromRefreshToken(refresh_token);
const decodedHeader = jwt.decode(access_token, { complete: true });
if (!decodedHeader || typeof decodedHeader !== 'object') {
throw new UnauthorizedException("Authentication failed")
}
const kid: any = decodedHeader.header.kid;
const publicKey = await this.getPublicKey(kid);
const decoded = jwt.verify(access_token, publicKey, { algorithms: ['RS256'] });
const email = (decoded && typeof decoded === 'object') ? (decoded as any).email : undefined;
return { ...tokens, email }
}
const response = await axios.post(this.TOKEN_URL, params, { const response = await axios.post(this.TOKEN_URL, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}); });
console.log('first time data ...');
let k = { ...response.data, email: username }; let k = { ...response.data, email: username };
return k; return k;
} catch (error) { } catch (error) {
console.log(error.message); console.log("error while logging : ", error.message);
throw new BadRequestException('Invalid username or password'); throw new BadRequestException('Invalid username or password');
} }
// }
// else if (accessToken && refreshToken) {
// return { access_token: accessToken, refresh_token: refreshToken, email: username }
// }
} }
async getUserIdByEmail(email: string) { async getUserIdByEmailX(email: string) {
let url = `${this.USERS_URL}?email=${email}`; let url = `${this.USERS_URL}?email=${email}`;
@ -289,6 +367,43 @@ export class AuthService {
} }
} }
async getUserIdByEmail(email: string): Promise<string> {
try {
const adminAccessToken = await this.getAdminAccessToken();
const url = `${this.USERS_URL}?email=${encodeURIComponent(email)}`;
const options: AxiosRequestConfig = {
method: 'GET',
url,
headers: {
Authorization: `Bearer ${adminAccessToken}`,
'Content-Type': 'application/json',
},
};
const response = await axios.request(options);
if (response.status !== 200 || !Array.isArray(response.data)) {
console.log("failed to retrieve user-id....");
throw new UnauthorizedException("Authentication failed")
}
const user = response.data[0];
if (user?.id) {
return user.id
} else {
throw new UnauthorizedException("Authentication failed")
}
} catch (error) {
console.log("Error in getUserIdByEmail ...........");
if (error instanceof UnauthorizedException) {
throw error
}
throw new InternalServerErrorException();
}
}
async forgotPassword() { async forgotPassword() {
const validatePasswordURLSearchParams = new URLSearchParams({ const validatePasswordURLSearchParams = new URLSearchParams({
@ -341,24 +456,28 @@ export class AuthService {
async getAdminAccessToken() { async getAdminAccessToken() {
try { try {
const adminParams = new URLSearchParams(); const adminParams = new URLSearchParams({
adminParams.append('grant_type', 'client_credentials'); grant_type: 'client_credentials',
adminParams.append('client_id', this.CLIENT_ID); client_id: this.CLIENT_ID,
adminParams.append('client_secret', this.CLIENT_SECRET); client_secret: this.CLIENT_SECRET,
const adminTokenResponse = await axios.post(this.TOKEN_URL,
adminParams,
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}); });
// console.log(adminTokenResponse.data);
const adminAccessToken = adminTokenResponse.data.access_token; const { data } = await axios.post(this.TOKEN_URL, adminParams, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
timeout: 6000, // optional: add a timeout to avoid hanging requests
});
if (!data.access_token) {
console.log("failed to retrieve admin access token.....");
throw new UnauthorizedException("Authentication failed")
}
return data.access_token;
return adminAccessToken;
} catch (error) { } catch (error) {
console.log(error.message); if (error instanceof UnauthorizedException) throw error
throw error console.log("Error in getAdminAccessToken");
throw new InternalServerErrorException();
} }
} }
@ -546,16 +665,13 @@ export class AuthService {
} }
console.log("refesh token present : ", refreshToken);
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('grant_type', 'refresh_token'); params.append('grant_type', 'refresh_token');
params.append('client_id', this.CLIENT_ID); params.append('client_id', this.CLIENT_ID);
params.append('client_secret', this.CLIENT_SECRET); params.append('client_secret', this.CLIENT_SECRET);
params.append('refresh_token', refreshToken); params.append('refresh_token', refreshToken);
console.log("url for refresh is : ", `${this.KEYCLOAK_URL}/auth/realms/${this.KEYCLOAK_REALM}/protocol/openid-connect/token`); // console.log("url for refresh is : ", `${this.KEYCLOAK_URL}/auth/realms/${this.KEYCLOAK_REALM}/protocol/openid-connect/token`);
try { try {
const response = await axios.post(this.TOKEN_URL, params, { const response = await axios.post(this.TOKEN_URL, params, {

View File

@ -8,7 +8,7 @@ import {
import { AuthService } from 'src/auth/auth.service'; import { AuthService } from 'src/auth/auth.service';
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken';
import { InternalServerException } from 'src/exceptions/internalServerError.exception'; import { InternalServerException } from 'src/exceptions/internalServerError.exception';
import { UnauthorizedException as UE} from 'src/exceptions/unauthorized.exception'; import { UnauthorizedException as UE } from 'src/exceptions/unauthorized.exception';
@Global() @Global()
@Injectable() @Injectable()
@ -20,30 +20,35 @@ export class JwtAuthGuard implements CanActivate {
const token = request.cookies?.access_token; const token = request.cookies?.access_token;
if (!token) { if (!token) {
console.log('❌ No access token found'); console.log("No token found from Request");
throw new UnauthorizedException('Authentication failed'); throw new UnauthorizedException('Authentication failed');
} }
console.log("✅ access_token is present in JwtAuthGuard"); // Decode the token without verifying the signature
const decoded = jwt.decode(token) as { exp?: number };
try { if (!decoded || !decoded.exp) {
const decoded = await this.authService.decodeToken(token); throw new UnauthorizedException('Authentication failed');
request.user = decoded; }
return true;
} catch (error) { const currentTime = Math.floor(Date.now() / 1000); // in seconds
if (
error instanceof jwt.TokenExpiredError || if (decoded.exp < currentTime) {
error instanceof jwt.JsonWebTokenError console.log('Token expired');
) {
console.log('❌ Token is invalid or expired');
throw new UnauthorizedException('Invalid Token'); throw new UnauthorizedException('Invalid Token');
} }
else if (error instanceof UnauthorizedException || error instanceof UE) {
// Token is valid and not expired - continue with your normal logic
try {
const verifiedUser = await this.authService.decodeToken(token);
request.user = verifiedUser;
return true;
} catch (error) {
if (error instanceof UnauthorizedException || error instanceof UE) {
throw error; throw error;
} }
console.log(error instanceof UE);
throw new InternalServerException(); throw new InternalServerException();
} }
} }
} }