n8n
Version:
n8n Workflow Automation Tool
267 lines • 12 kB
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);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OAuthTokenService = void 0;
const errors_js_1 = require("@modelcontextprotocol/sdk/server/auth/errors.js");
const backend_common_1 = require("@n8n/backend-common");
const constants_1 = require("@n8n/constants");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const ensure_error_1 = require("@n8n/utils/errors/ensure-error");
const n8n_workflow_1 = require("n8n-workflow");
const node_crypto_1 = require("node:crypto");
const jwt_service_1 = require("../../services/jwt.service");
const protected_resource_registry_1 = require("../../services/protected-resource.registry");
const oauth_access_token_repository_1 = require("./database/repositories/oauth-access-token.repository");
const oauth_refresh_token_repository_1 = require("./database/repositories/oauth-refresh-token.repository");
const oauth_errors_1 = require("./oauth.errors");
let OAuthTokenService = class OAuthTokenService {
constructor(logger, jwtService, userRepository, accessTokenRepository, refreshTokenRepository, resourceRegistry, txRunner) {
this.logger = logger;
this.jwtService = jwtService;
this.userRepository = userRepository;
this.accessTokenRepository = accessTokenRepository;
this.refreshTokenRepository = refreshTokenRepository;
this.resourceRegistry = resourceRegistry;
this.txRunner = txRunner;
this.ACCESS_TOKEN_EXPIRY_SECONDS = 1 * constants_1.Time.hours.toSeconds;
this.REFRESH_TOKEN_EXPIRY_MS = 30 * constants_1.Time.days.toMilliseconds;
}
getAccessTokenExpirySeconds() {
return this.ACCESS_TOKEN_EXPIRY_SECONDS;
}
generateTokenPair(userId, clientId, resource, scopes) {
const audience = resource ?? this.resourceRegistry.getDefaultResource()?.getResourceUrl();
if (!audience) {
throw new n8n_workflow_1.UnexpectedError('Cannot mint an OAuth access token: no resource requested and no default protected resource is registered');
}
const accessToken = this.jwtService.sign({
sub: userId,
aud: audience,
client_id: clientId,
jti: (0, node_crypto_1.randomUUID)(),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + this.ACCESS_TOKEN_EXPIRY_SECONDS,
scope: scopes.join(' '),
meta: {
isOAuth: true,
},
});
const refreshToken = (0, node_crypto_1.randomBytes)(32).toString('hex');
return { accessToken, refreshToken };
}
async saveTokenPair(accessToken, refreshToken, clientId, userId, scopes) {
await this.txRunner.run({}, async (ctx) => {
await this.accessTokenRepository.insertToken({ token: accessToken, clientId, userId }, ctx);
await this.refreshTokenRepository.insertToken({
token: refreshToken,
clientId,
userId,
expiresAt: Date.now() + this.REFRESH_TOKEN_EXPIRY_MS,
scope: scopes,
}, ctx);
});
}
async validateAndRotateRefreshToken(refreshToken, clientId, resource) {
return await this.txRunner.run({}, async (ctx) => {
const now = Date.now();
const refreshTokenRecord = await this.refreshTokenRepository.findByToken(refreshToken, clientId, ctx);
if (!refreshTokenRecord) {
throw new errors_js_1.InvalidGrantError('Invalid refresh token');
}
const numAffected = await this.refreshTokenRepository.deleteValidByToken(refreshToken, clientId, now, ctx);
if (numAffected < 1) {
throw new errors_js_1.InvalidGrantError('Invalid refresh token');
}
const scopes = refreshTokenRecord.scope;
const { accessToken, refreshToken: newRefreshToken } = this.generateTokenPair(refreshTokenRecord.userId, clientId, resource, scopes);
await this.accessTokenRepository.insertToken({ token: accessToken, clientId, userId: refreshTokenRecord.userId }, ctx);
await this.refreshTokenRepository.insertToken({
token: newRefreshToken,
clientId,
userId: refreshTokenRecord.userId,
expiresAt: now + this.REFRESH_TOKEN_EXPIRY_MS,
scope: scopes,
}, ctx);
this.logger.info('Refresh token rotated and new access token issued', {
clientId,
userId: refreshTokenRecord.userId,
});
return {
access_token: accessToken,
token_type: 'Bearer',
expires_in: this.ACCESS_TOKEN_EXPIRY_SECONDS,
refresh_token: newRefreshToken,
scope: scopes.join(' '),
};
});
}
async verifyAccessToken(token, expectedAudience) {
return await this.verifyTokenWithAudiences(token, await this.getAllowedAudiences(expectedAudience));
}
async verifyTokenWithAudiences(token, allowedAudiences) {
let decoded;
try {
decoded = this.verifyJwtWithAllowedAudiences(token, allowedAudiences);
}
catch (error) {
throw new oauth_errors_1.JWTVerificationError();
}
const clientId = this.getStringClaim(decoded, 'client_id');
const userId = this.getStringClaim(decoded, 'sub');
if (!clientId || !userId) {
throw new oauth_errors_1.JWTVerificationError();
}
const accessTokenRecord = await this.accessTokenRepository.findOne({
where: { token },
});
if (!accessTokenRecord) {
throw new oauth_errors_1.AccessTokenNotFoundError();
}
return {
token,
clientId,
scopes: this.parseScopeClaim(decoded),
extra: {
userId,
},
};
}
parseScopeClaim(decoded) {
const scopeClaim = this.getStringClaim(decoded, 'scope');
if (scopeClaim === null) {
return [];
}
return scopeClaim === '' ? [] : scopeClaim.split(' ');
}
async verifyOAuthAccessToken(token, expectedAudience) {
try {
const resource = await this.getResourceByAudience(expectedAudience);
if (expectedAudience && !resource) {
return { user: null, context: { reason: 'insufficient_scope', auth_type: 'oauth' } };
}
const authInfo = await this.verifyTokenWithAudiences(token, this.audiencesForResource(resource, expectedAudience));
const userId = authInfo.extra && typeof authInfo.extra === 'object'
? this.getStringClaim(authInfo.extra, 'userId')
: null;
if (!userId) {
return { user: null, context: { reason: 'user_id_not_in_auth_info', auth_type: 'oauth' } };
}
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['role'],
});
if (!user) {
return { user: null, context: { reason: 'user_not_found', auth_type: 'oauth' } };
}
if (resource && !(await resource.authorize(user))) {
this.logger.warn('OAuth token denied: user lacks execute access', {
userId: user.id,
expectedAudience,
});
return { user: null, context: { reason: 'insufficient_scope', auth_type: 'oauth' } };
}
return { user, authType: 'oauth', scopes: authInfo.scopes };
}
catch (error) {
const errorForSure = (0, ensure_error_1.ensureError)(error);
const reason = errorForSure instanceof oauth_errors_1.JWTVerificationError
? 'invalid_token'
: errorForSure instanceof oauth_errors_1.AccessTokenNotFoundError
? 'token_not_found_in_db'
: 'unknown_error';
return {
user: null,
context: {
reason,
auth_type: 'oauth',
error_details: errorForSure.message,
},
};
}
}
async revokeAllTokensForGrant(clientId, userId) {
await Promise.all([
this.accessTokenRepository.delete({ clientId, userId }),
this.refreshTokenRepository.delete({ clientId, userId }),
]);
}
async revokeAccessToken(token, clientId) {
const result = await this.accessTokenRepository.delete({
token,
clientId,
});
const revoked = (result.affected ?? 0) > 0;
if (revoked) {
this.logger.info('Access token revoked', { clientId });
}
return revoked;
}
async revokeRefreshToken(token, clientId) {
const result = await this.refreshTokenRepository.delete({
token,
clientId,
});
const revoked = (result.affected ?? 0) > 0;
if (revoked) {
this.logger.info('Refresh token revoked', { clientId });
}
return revoked;
}
async getAllowedAudiences(expectedAudience) {
const resource = expectedAudience
? await this.resourceRegistry.getByResourceUrl(expectedAudience)
: undefined;
return this.audiencesForResource(resource, expectedAudience);
}
audiencesForResource(resource, expectedAudience) {
if (expectedAudience) {
return resource ? resource.getAudiences() : [expectedAudience];
}
return this.resourceRegistry.getAllAudiences();
}
async getResourceByAudience(expectedAudience) {
if (!expectedAudience) {
return this.resourceRegistry.getDefaultResource();
}
return await this.resourceRegistry.getByResourceUrl(expectedAudience);
}
verifyJwtWithAllowedAudiences(token, audiences) {
try {
return this.jwtService.verify(token, {
audience: audiences,
});
}
catch (error) {
for (const audience of audiences) {
try {
return this.jwtService.verify(token, { audience });
}
catch {
continue;
}
}
throw error;
}
}
getStringClaim(payload, claim) {
if (!payload || typeof payload !== 'object')
return null;
const claimValue = payload[claim];
return typeof claimValue === 'string' ? claimValue : null;
}
};
exports.OAuthTokenService = OAuthTokenService;
exports.OAuthTokenService = OAuthTokenService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger, jwt_service_1.JwtService, db_1.UserRepository, oauth_access_token_repository_1.AccessTokenRepository, oauth_refresh_token_repository_1.RefreshTokenRepository, protected_resource_registry_1.ProtectedResourceRegistry, db_1.TransactionRunner])
], OAuthTokenService);
//# sourceMappingURL=oauth-token.service.js.map