fix: v2 token secret conflicts (#21279)

* fix: accessToken.create unique constraint failed on the secret field

* fix: force refresh tokens within single transaction
This commit is contained in:
Lauris Skraucis
2025-05-15 16:04:50 +00:00
committed by GitHub
parent df9b72583d
commit 6994cac5e0
2 changed files with 68 additions and 22 deletions
@@ -177,7 +177,7 @@ export class OAuthClientUsersController {
const { id } = await this.validateManagedUserOwnership(oAuthClientId, userId);
const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } =
await this.tokensRepository.createOAuthTokens(oAuthClientId, id, true);
await this.tokensRepository.forceRefreshOAuthTokens(oAuthClientId, id);
return {
status: SUCCESS_STATUS,
@@ -5,6 +5,7 @@ import { Injectable } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { PlatformAuthorizationToken } from "@prisma/client";
import { DateTime } from "luxon";
import { v4 as uuidv4 } from "uuid";
@Injectable()
export class TokensRepository {
@@ -50,31 +51,18 @@ export class TokensRepository {
});
}
async createOAuthTokens(clientId: string, ownerId: number, deleteOld?: boolean) {
if (deleteOld) {
try {
await this.dbWrite.prisma.$transaction([
this.dbWrite.prisma.accessToken.deleteMany({
where: { client: { id: clientId }, userId: ownerId },
}),
this.dbWrite.prisma.refreshToken.deleteMany({
where: {
client: { id: clientId },
userId: ownerId,
},
}),
]);
} catch (err) {
this.logger.error("createOAuthTokens - Failed to delete old tokens", err);
}
}
async createOAuthTokens(clientId: string, ownerId: number) {
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([
this.dbWrite.prisma.accessToken.create({
data: {
secret: this.jwtService.signAccessToken({ clientId, ownerId, expiresAt: accessExpiry.valueOf() }),
secret: this.jwtService.signAccessToken({
clientId,
ownerId,
expiresAt: accessExpiry.valueOf(),
jti: uuidv4(),
}),
expiresAt: accessExpiry,
client: { connect: { id: clientId } },
owner: { connect: { id: ownerId } },
@@ -82,7 +70,12 @@ export class TokensRepository {
}),
this.dbWrite.prisma.refreshToken.create({
data: {
secret: this.jwtService.signRefreshToken({ clientId, ownerId, expiresAt: refreshExpiry.valueOf() }),
secret: this.jwtService.signRefreshToken({
clientId,
ownerId,
expiresAt: refreshExpiry.valueOf(),
jti: uuidv4(),
}),
expiresAt: refreshExpiry,
client: { connect: { id: clientId } },
owner: { connect: { id: ownerId } },
@@ -98,6 +91,57 @@ export class TokensRepository {
};
}
async forceRefreshOAuthTokens(clientId: string, ownerId: number) {
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
const [_deletedAccessToken, _deletedRefreshToken, accessToken, refreshToken] =
await this.dbWrite.prisma.$transaction([
this.dbWrite.prisma.accessToken.deleteMany({
where: { client: { id: clientId }, userId: ownerId },
}),
this.dbWrite.prisma.refreshToken.deleteMany({
where: {
client: { id: clientId },
userId: ownerId,
},
}),
this.dbWrite.prisma.accessToken.create({
data: {
secret: this.jwtService.signAccessToken({
clientId,
ownerId,
expiresAt: accessExpiry.valueOf(),
jti: uuidv4(),
}),
expiresAt: accessExpiry,
client: { connect: { id: clientId } },
owner: { connect: { id: ownerId } },
},
}),
this.dbWrite.prisma.refreshToken.create({
data: {
secret: this.jwtService.signRefreshToken({
clientId,
ownerId,
expiresAt: refreshExpiry.valueOf(),
jti: uuidv4(),
}),
expiresAt: refreshExpiry,
client: { connect: { id: clientId } },
owner: { connect: { id: ownerId } },
},
}),
]);
return {
accessToken: accessToken.secret,
accessTokenExpiresAt: accessToken.expiresAt,
refreshToken: refreshToken.secret,
refreshTokenExpiresAt: refreshToken.expiresAt,
};
}
async getAccessTokenExpiryDate(accessTokenSecret: string) {
const accessToken = await this.dbRead.prisma.accessToken.findFirst({
where: {
@@ -140,6 +184,7 @@ export class TokensRepository {
ownerId: tokenUserId,
expiresAt: accessExpiry.valueOf(),
userId: tokenUserId,
jti: uuidv4(),
}),
expiresAt: accessExpiry,
client: { connect: { id: clientId } },
@@ -155,6 +200,7 @@ export class TokensRepository {
ownerId: tokenUserId,
expiresAt: refreshExpiry.valueOf(),
userId: tokenUserId,
jti: uuidv4(),
}),
expiresAt: refreshExpiry,
client: { connect: { id: clientId } },