n8n
Version:
n8n Workflow Automation Tool
203 lines • 8.71 kB
JavaScript
;
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.McpOAuthTokenService = void 0;
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 typeorm_1 = require("@n8n/typeorm");
const n8n_workflow_1 = require("n8n-workflow");
const node_crypto_1 = require("node:crypto");
const oauth_access_token_entity_1 = require("./database/entities/oauth-access-token.entity");
const oauth_refresh_token_entity_1 = require("./database/entities/oauth-refresh-token.entity");
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 mcp_errors_1 = require("./mcp.errors");
const jwt_service_1 = require("../../services/jwt.service");
let McpOAuthTokenService = class McpOAuthTokenService {
constructor(logger, jwtService, userRepository, accessTokenRepository, refreshTokenRepository) {
this.logger = logger;
this.jwtService = jwtService;
this.userRepository = userRepository;
this.accessTokenRepository = accessTokenRepository;
this.refreshTokenRepository = refreshTokenRepository;
this.MCP_AUDIENCE = 'mcp-server-api';
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) {
const accessToken = this.jwtService.sign({
sub: userId,
aud: this.MCP_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,
meta: {
isOAuth: true,
},
});
const refreshToken = (0, node_crypto_1.randomBytes)(32).toString('hex');
return { accessToken, refreshToken };
}
async saveTokenPair(accessToken, refreshToken, clientId, userId) {
await this.accessTokenRepository.manager.transaction(async (transactionManager) => {
await transactionManager.insert(this.accessTokenRepository.target, {
token: accessToken,
clientId,
userId,
});
await transactionManager.insert(this.refreshTokenRepository.target, {
token: refreshToken,
clientId,
userId,
expiresAt: Date.now() + this.REFRESH_TOKEN_EXPIRY_MS,
});
});
}
async validateAndRotateRefreshToken(refreshToken, clientId) {
return await (0, db_1.withTransaction)(this.refreshTokenRepository.manager, undefined, async (trx) => {
const now = Date.now();
const refreshTokenRecord = await trx.findOne(oauth_refresh_token_entity_1.RefreshToken, {
where: {
token: refreshToken,
clientId,
},
});
if (!refreshTokenRecord) {
throw new Error('Invalid refresh token');
}
const result = await trx.delete(oauth_refresh_token_entity_1.RefreshToken, {
token: refreshToken,
clientId,
expiresAt: (0, typeorm_1.MoreThanOrEqual)(now),
});
const numAffected = result.affected ?? 0;
if (numAffected < 1) {
throw new Error('Invalid refresh token');
}
const { accessToken, refreshToken: newRefreshToken } = this.generateTokenPair(refreshTokenRecord.userId, clientId);
await trx.insert(oauth_access_token_entity_1.AccessToken, {
token: accessToken,
clientId,
userId: refreshTokenRecord.userId,
});
await trx.insert(oauth_refresh_token_entity_1.RefreshToken, {
token: newRefreshToken,
clientId,
userId: refreshTokenRecord.userId,
expiresAt: now + this.REFRESH_TOKEN_EXPIRY_MS,
});
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,
};
});
}
async verifyAccessToken(token) {
let decoded;
try {
decoded = this.jwtService.verify(token, { audience: this.MCP_AUDIENCE });
}
catch (error) {
throw new mcp_errors_1.JWTVerificationError();
}
const accessTokenRecord = await this.accessTokenRepository.findOne({
where: { token },
});
if (!accessTokenRecord) {
throw new mcp_errors_1.AccessTokenNotFoundError();
}
return {
token,
clientId: decoded.client_id,
scopes: [],
extra: {
userId: decoded.sub,
},
};
}
async verifyOAuthAccessToken(token) {
try {
const authInfo = await this.verifyAccessToken(token);
const userId = authInfo.extra?.userId;
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' } };
}
return { user, authType: 'oauth' };
}
catch (error) {
const errorForSure = (0, n8n_workflow_1.ensureError)(error);
const reason = errorForSure instanceof mcp_errors_1.JWTVerificationError
? 'invalid_token'
: errorForSure instanceof mcp_errors_1.AccessTokenNotFoundError
? 'token_not_found_in_db'
: 'unknown_error';
return {
user: null,
context: {
reason,
auth_type: 'oauth',
error_details: errorForSure.message,
},
};
}
}
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;
}
};
exports.McpOAuthTokenService = McpOAuthTokenService;
exports.McpOAuthTokenService = McpOAuthTokenService = __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])
], McpOAuthTokenService);
//# sourceMappingURL=mcp-oauth-token.service.js.map