209 lines
9.4 KiB
JavaScript
209 lines
9.4 KiB
JavaScript
"use strict";
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
};
|
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.AuthService = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const config_1 = require("@nestjs/config");
|
|
const jwt_1 = require("@nestjs/jwt");
|
|
const sync_identity_handler_1 = require("../identity/application/handlers/user/sync-identity.handler");
|
|
const user_interface_1 = require("../identity/domain/repositories/user.interface");
|
|
const prisma_service_1 = require("../../shared/prisma.service");
|
|
const redis_pkce_store_1 = require("./pkce/redis-pkce.store");
|
|
const inmemory_refresh_store_1 = require("./refresh/inmemory-refresh.store");
|
|
const openidClient = require('openid-client');
|
|
let AuthService = class AuthService {
|
|
config;
|
|
jwtService;
|
|
prisma;
|
|
userRepository;
|
|
syncIdentityHandler;
|
|
pkceStore;
|
|
refreshStore;
|
|
constructor(config, jwtService, prisma, userRepository, syncIdentityHandler, pkceStore, refreshStore) {
|
|
this.config = config;
|
|
this.jwtService = jwtService;
|
|
this.prisma = prisma;
|
|
this.userRepository = userRepository;
|
|
this.syncIdentityHandler = syncIdentityHandler;
|
|
this.pkceStore = pkceStore;
|
|
this.refreshStore = refreshStore;
|
|
}
|
|
issuer = null;
|
|
client = null;
|
|
async getIssuer() {
|
|
if (this.issuer)
|
|
return this.issuer;
|
|
const issuerUrl = this.config.get('AUTHENTIK_ISSUER');
|
|
if (!issuerUrl)
|
|
throw new Error('AUTHENTIK_ISSUER not configured');
|
|
this.issuer = await openidClient.Issuer.discover(issuerUrl);
|
|
return this.issuer;
|
|
}
|
|
async getClient() {
|
|
if (this.client)
|
|
return this.client;
|
|
const issuer = await this.getIssuer();
|
|
const clientId = this.config.get('AUTHENTIK_CLIENT_ID');
|
|
const clientSecret = this.config.get('AUTHENTIK_CLIENT_SECRET');
|
|
if (!clientId)
|
|
throw new Error('AUTHENTIK_CLIENT_ID not configured');
|
|
this.client = new issuer.Client({ client_id: clientId, client_secret: clientSecret });
|
|
return this.client;
|
|
}
|
|
async createAuthorizationRedirect(returnTo) {
|
|
const client = await this.getClient();
|
|
const redirectUri = this.config.get('AUTH_CALLBACK_URL') || `${this.config.get('APP_URL') || ''}/auth/callback`;
|
|
const state = require('crypto').randomUUID();
|
|
const code_verifier = openidClient.generators.codeVerifier();
|
|
const code_challenge = await openidClient.generators.codeChallenge(code_verifier);
|
|
const nonce = openidClient.generators.nonce();
|
|
await this.pkceStore.save(state, { code_verifier, nonce, returnTo }, 300);
|
|
const url = client.authorizationUrl({
|
|
redirect_uri: redirectUri,
|
|
scope: this.config.get('AUTHENTIK_DEFAULT_SCOPE') || 'openid email profile',
|
|
response_type: 'code',
|
|
code_challenge,
|
|
code_challenge_method: 'S256',
|
|
state,
|
|
nonce,
|
|
});
|
|
return url;
|
|
}
|
|
async handleCallback(code, state) {
|
|
const client = await this.getClient();
|
|
const pkce = await this.pkceStore.get(state);
|
|
if (!pkce)
|
|
throw new common_1.UnauthorizedException('Invalid or expired state');
|
|
// remove one-time state
|
|
await this.pkceStore.remove(state);
|
|
const redirectUri = this.config.get('AUTH_CALLBACK_URL') || `${this.config.get('APP_URL') || ''}/auth/callback`;
|
|
// exchange code
|
|
const tokenSet = await client.callback(redirectUri, { code, state }, { code_verifier: pkce.code_verifier, nonce: pkce.nonce });
|
|
// verify id_token and get claims
|
|
const claims = tokenSet.claims();
|
|
// fetch userinfo if available
|
|
let userInfo = null;
|
|
try {
|
|
if (tokenSet.access_token && client.userinfo) {
|
|
userInfo = await client.userinfo(tokenSet.access_token);
|
|
}
|
|
}
|
|
catch (e) {
|
|
// ignore
|
|
}
|
|
const identity = {
|
|
sub: userInfo?.sub || claims.sub || null,
|
|
preferred_username: userInfo?.preferred_username || userInfo?.username || userInfo?.email || claims.preferred_username || claims.email,
|
|
email: userInfo?.email || claims.email,
|
|
raw: { tokenSet, userInfo, claims },
|
|
};
|
|
// Sync identity to local user (create if needed)
|
|
const domainUser = await this.syncIdentityHandler.execute(identity);
|
|
// ensure active/not deleted
|
|
if (!domainUser.isActive)
|
|
throw new common_1.UnauthorizedException('User is not active');
|
|
if (domainUser.deletedAt)
|
|
throw new common_1.UnauthorizedException('User is deleted');
|
|
// create internal JWT
|
|
const jwtPayload = {
|
|
sub: domainUser.id,
|
|
preferred_username: domainUser.username,
|
|
email: domainUser.email,
|
|
};
|
|
const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600);
|
|
const access = this.jwtService.sign(jwtPayload, { expiresIn });
|
|
// create internal refresh token
|
|
const refreshToken = require('crypto').randomUUID();
|
|
const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600); // default 30 days
|
|
await this.refreshStore.set(refreshToken, { userId: domainUser.id }, refreshTtl);
|
|
return {
|
|
accessToken: access,
|
|
refreshToken,
|
|
expiresIn,
|
|
refreshTtl,
|
|
user: {
|
|
id: domainUser.id,
|
|
username: domainUser.username,
|
|
email: domainUser.email,
|
|
roles: domainUser.roles || [],
|
|
},
|
|
returnTo: pkce.returnTo,
|
|
};
|
|
}
|
|
async refresh(refreshToken) {
|
|
if (!refreshToken)
|
|
throw new common_1.UnauthorizedException('Missing refresh token');
|
|
const data = await this.refreshStore.get(refreshToken);
|
|
if (!data)
|
|
throw new common_1.UnauthorizedException('Invalid refresh token');
|
|
const userId = data.userId;
|
|
// load user
|
|
const domainUser = await this.userRepository.getById(userId);
|
|
if (!domainUser)
|
|
throw new common_1.UnauthorizedException('User not found');
|
|
if (!domainUser.isActive)
|
|
throw new common_1.UnauthorizedException('User is not active');
|
|
// rotate refresh token
|
|
await this.refreshStore.del(refreshToken);
|
|
const newRefresh = require('crypto').randomUUID();
|
|
const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600);
|
|
await this.refreshStore.set(newRefresh, { userId }, refreshTtl);
|
|
const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600);
|
|
const access = this.jwtService.sign({ sub: domainUser.id, preferred_username: domainUser.username, email: domainUser.email }, { expiresIn });
|
|
return { accessToken: access, refreshToken: newRefresh, expiresIn, refreshTtl };
|
|
}
|
|
async logout(refreshToken) {
|
|
if (refreshToken) {
|
|
await this.refreshStore.del(refreshToken);
|
|
}
|
|
const issuer = await this.getIssuer();
|
|
const endSession = issuer.metadata.end_session_endpoint;
|
|
const postLogout = this.config.get('APP_URL') || '/';
|
|
if (endSession) {
|
|
// Redirect to identity provider logout
|
|
const url = new URL(endSession);
|
|
if (postLogout)
|
|
url.searchParams.set('post_logout_redirect_uri', postLogout);
|
|
return url.toString();
|
|
}
|
|
return postLogout;
|
|
}
|
|
async me(token) {
|
|
if (!token)
|
|
throw new common_1.UnauthorizedException('Missing token');
|
|
try {
|
|
const payload = this.jwtService.verify(token);
|
|
const user = await this.userRepository.getById(payload.sub);
|
|
if (!user)
|
|
throw new common_1.UnauthorizedException('User not found');
|
|
return { id: user.id, username: user.username, email: user.email, roles: user.roles || [] };
|
|
}
|
|
catch (e) {
|
|
throw new common_1.UnauthorizedException('Invalid token');
|
|
}
|
|
}
|
|
};
|
|
exports.AuthService = AuthService;
|
|
exports.AuthService = AuthService = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__param(3, (0, common_1.Inject)(user_interface_1.IUser)),
|
|
__metadata("design:paramtypes", [config_1.ConfigService,
|
|
jwt_1.JwtService,
|
|
prisma_service_1.PrismaService,
|
|
user_interface_1.IUser,
|
|
sync_identity_handler_1.SyncIdentityHandler,
|
|
redis_pkce_store_1.RedisPkceStore,
|
|
inmemory_refresh_store_1.InMemoryRefreshStore])
|
|
], AuthService);
|
|
//# sourceMappingURL=auth.service.js.map
|