Add authentication runtime pipeline
This commit is contained in:
parent
e4edf71955
commit
2ccb54f978
@ -3,6 +3,7 @@ import { PrismaModule } from '../../infrastructure/database/prisma.module';
|
|||||||
import { RequestContextMiddleware } from '../../infrastructure/request-context/request-context.middleware';
|
import { RequestContextMiddleware } from '../../infrastructure/request-context/request-context.middleware';
|
||||||
import { RequestContextModule } from '../../infrastructure/request-context/request-context.module';
|
import { RequestContextModule } from '../../infrastructure/request-context/request-context.module';
|
||||||
import { AccessTokenService } from './access-token.service';
|
import { AccessTokenService } from './access-token.service';
|
||||||
|
import { AuthenticationRuntimeService } from './authentication-runtime.service';
|
||||||
import { DeviceAuthGuard } from './device-auth.guard';
|
import { DeviceAuthGuard } from './device-auth.guard';
|
||||||
import { DeviceAuthService } from './device-auth.service';
|
import { DeviceAuthService } from './device-auth.service';
|
||||||
import { OptionalDeviceAuthGuard } from './optional-device-auth.guard';
|
import { OptionalDeviceAuthGuard } from './optional-device-auth.guard';
|
||||||
@ -14,6 +15,7 @@ import { SessionService } from './session.service';
|
|||||||
imports: [PrismaModule, RequestContextModule],
|
imports: [PrismaModule, RequestContextModule],
|
||||||
providers: [
|
providers: [
|
||||||
AccessTokenService,
|
AccessTokenService,
|
||||||
|
AuthenticationRuntimeService,
|
||||||
DeviceAuthService,
|
DeviceAuthService,
|
||||||
DeviceAuthGuard,
|
DeviceAuthGuard,
|
||||||
OptionalDeviceAuthGuard,
|
OptionalDeviceAuthGuard,
|
||||||
@ -23,6 +25,7 @@ import { SessionService } from './session.service';
|
|||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AccessTokenService,
|
AccessTokenService,
|
||||||
|
AuthenticationRuntimeService,
|
||||||
DeviceAuthService,
|
DeviceAuthService,
|
||||||
DeviceAuthGuard,
|
DeviceAuthGuard,
|
||||||
OptionalDeviceAuthGuard,
|
OptionalDeviceAuthGuard,
|
||||||
|
|||||||
328
backend/src/modules/auth/authentication-runtime.service.spec.ts
Normal file
328
backend/src/modules/auth/authentication-runtime.service.spec.ts
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
import { createHash, randomUUID } from 'node:crypto';
|
||||||
|
import { UnauthorizedException } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
AccountSessionStatus,
|
||||||
|
UserAccountStatus,
|
||||||
|
} from '@prisma/client';
|
||||||
|
import { AppConfigService } from '../config/config.service';
|
||||||
|
import { AccessTokenService } from './access-token.service';
|
||||||
|
import { AuthenticationRuntimeService } from './authentication-runtime.service';
|
||||||
|
import { RefreshTokenService } from './refresh-token.service';
|
||||||
|
import { SessionService } from './session.service';
|
||||||
|
|
||||||
|
describe('AuthenticationRuntimeService', () => {
|
||||||
|
const userId = randomUUID();
|
||||||
|
const otherUserId = randomUUID();
|
||||||
|
const accessTokenTtlSeconds = 900;
|
||||||
|
const sessionExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const buildService = () => {
|
||||||
|
const users = new Map<string, any>([
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
id: userId,
|
||||||
|
accountStatus: UserAccountStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
otherUserId,
|
||||||
|
{
|
||||||
|
id: otherUserId,
|
||||||
|
accountStatus: UserAccountStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
const sessions = new Map<string, any>();
|
||||||
|
|
||||||
|
const accountSession = {
|
||||||
|
create: jest.fn().mockImplementation(async ({ data }) => {
|
||||||
|
const now = new Date();
|
||||||
|
const session = {
|
||||||
|
id: randomUUID(),
|
||||||
|
userId: data.userId,
|
||||||
|
refreshTokenHash: data.refreshTokenHash,
|
||||||
|
status: data.status ?? AccountSessionStatus.ACTIVE,
|
||||||
|
accessTokenVersion: data.accessTokenVersion ?? 1,
|
||||||
|
expiresAt: data.expiresAt,
|
||||||
|
lastRefreshedAt: null,
|
||||||
|
lastSeenAt: null,
|
||||||
|
revokedAt: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
sessions.set(session.id, session);
|
||||||
|
return { ...session };
|
||||||
|
}),
|
||||||
|
findUnique: jest.fn().mockImplementation(async ({ where, include }) => {
|
||||||
|
const session =
|
||||||
|
(where.id ? sessions.get(where.id) : undefined) ??
|
||||||
|
Array.from(sessions.values()).find(
|
||||||
|
(candidate) =>
|
||||||
|
where.refreshTokenHash !== undefined &&
|
||||||
|
candidate.refreshTokenHash === where.refreshTokenHash,
|
||||||
|
) ??
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include?.user) {
|
||||||
|
return {
|
||||||
|
...session,
|
||||||
|
user: users.get(session.userId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...session };
|
||||||
|
}),
|
||||||
|
update: jest.fn().mockImplementation(async ({ where, data }) => {
|
||||||
|
const session = sessions.get(where.id);
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(session, data, { updatedAt: new Date() });
|
||||||
|
return { ...session };
|
||||||
|
}),
|
||||||
|
updateMany: jest.fn().mockImplementation(async ({ where, data }) => {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const session of sessions.values()) {
|
||||||
|
const user = users.get(session.userId);
|
||||||
|
const matches =
|
||||||
|
(where.id === undefined || session.id === where.id) &&
|
||||||
|
(where.userId === undefined || session.userId === where.userId) &&
|
||||||
|
(where.refreshTokenHash === undefined ||
|
||||||
|
session.refreshTokenHash === where.refreshTokenHash) &&
|
||||||
|
(where.status === undefined || session.status === where.status) &&
|
||||||
|
(where.expiresAt?.gt === undefined ||
|
||||||
|
session.expiresAt.getTime() > where.expiresAt.gt.getTime()) &&
|
||||||
|
(where.user?.accountStatus === undefined ||
|
||||||
|
user?.accountStatus === where.user.accountStatus);
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
if (value && typeof value === 'object' && 'increment' in value) {
|
||||||
|
session[key] += (value as { increment: number }).increment;
|
||||||
|
} else {
|
||||||
|
session[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.updatedAt = new Date();
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const prismaService = {
|
||||||
|
user: {
|
||||||
|
findUnique: jest.fn().mockImplementation(async ({ where }) =>
|
||||||
|
users.get(where.id) ?? null,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
accountSession,
|
||||||
|
} as any;
|
||||||
|
const configService = {
|
||||||
|
accountAccessTokenSecret: 'runtime-access-token-test-secret',
|
||||||
|
accountAccessTokenTtlSeconds: accessTokenTtlSeconds,
|
||||||
|
} as AppConfigService;
|
||||||
|
const sessionService = new SessionService(prismaService);
|
||||||
|
const refreshTokenService = new RefreshTokenService(
|
||||||
|
prismaService,
|
||||||
|
sessionService,
|
||||||
|
);
|
||||||
|
const accessTokenService = new AccessTokenService(
|
||||||
|
configService,
|
||||||
|
sessionService,
|
||||||
|
);
|
||||||
|
const service = new AuthenticationRuntimeService(
|
||||||
|
sessionService,
|
||||||
|
accessTokenService,
|
||||||
|
refreshTokenService,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
service,
|
||||||
|
sessions,
|
||||||
|
accessTokenService,
|
||||||
|
refreshTokenService,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
it('creates an authenticated session with access and refresh tokens', async () => {
|
||||||
|
const { service, accessTokenService } = buildService();
|
||||||
|
|
||||||
|
const result = await service.createAuthenticatedSession(userId, {
|
||||||
|
expiresAt: sessionExpiresAt,
|
||||||
|
});
|
||||||
|
const claims = accessTokenService.decodeAccessToken(result.accessToken);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
userId,
|
||||||
|
sessionId: expect.any(String),
|
||||||
|
accessToken: expect.any(String),
|
||||||
|
refreshToken: expect.stringMatching(/^[A-Za-z0-9_-]{43}$/),
|
||||||
|
accessTokenExpiresAt: expect.any(Date),
|
||||||
|
sessionExpiresAt,
|
||||||
|
});
|
||||||
|
expect(claims).toMatchObject({
|
||||||
|
sub: userId,
|
||||||
|
sid: result.sessionId,
|
||||||
|
ver: 1,
|
||||||
|
typ: 'account_access',
|
||||||
|
});
|
||||||
|
expect(result.accessTokenExpiresAt.toISOString()).toBe(
|
||||||
|
new Date((claims.exp as number) * 1000).toISOString(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns no token hashes, password fields, or provider internals', async () => {
|
||||||
|
const { service } = buildService();
|
||||||
|
|
||||||
|
const result = await service.createAuthenticatedSession(userId, {
|
||||||
|
expiresAt: sessionExpiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Object.keys(result).sort()).toEqual(
|
||||||
|
[
|
||||||
|
'accessToken',
|
||||||
|
'accessTokenExpiresAt',
|
||||||
|
'refreshToken',
|
||||||
|
'sessionExpiresAt',
|
||||||
|
'sessionId',
|
||||||
|
'userId',
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
expect(result).not.toHaveProperty('refreshTokenHash');
|
||||||
|
expect(result).not.toHaveProperty('password');
|
||||||
|
expect(result).not.toHaveProperty('passwordHash');
|
||||||
|
expect(result).not.toHaveProperty('oauthProvider');
|
||||||
|
expect(result).not.toHaveProperty('providerSubject');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rotates the refresh token and issues a new versioned access token', async () => {
|
||||||
|
const { service, accessTokenService, refreshTokenService } = buildService();
|
||||||
|
const created = await service.createAuthenticatedSession(userId, {
|
||||||
|
expiresAt: sessionExpiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshed = await service.refreshAuthenticatedSession(
|
||||||
|
created.sessionId,
|
||||||
|
created.refreshToken,
|
||||||
|
);
|
||||||
|
const claims = accessTokenService.decodeAccessToken(
|
||||||
|
refreshed.accessToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(refreshed.accessToken).not.toBe(created.accessToken);
|
||||||
|
expect(refreshed.refreshToken).not.toBe(created.refreshToken);
|
||||||
|
expect(claims).toMatchObject({
|
||||||
|
sub: userId,
|
||||||
|
sid: created.sessionId,
|
||||||
|
ver: 2,
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
refreshTokenService.verifyRefreshToken(created.refreshToken),
|
||||||
|
).rejects.toThrow(UnauthorizedException);
|
||||||
|
await expect(
|
||||||
|
refreshTokenService.verifyRefreshToken(refreshed.refreshToken),
|
||||||
|
).resolves.toMatchObject({ id: created.sessionId });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects refresh after revoking one session and retains its history', async () => {
|
||||||
|
const { service, sessions } = buildService();
|
||||||
|
const created = await service.createAuthenticatedSession(userId, {
|
||||||
|
expiresAt: sessionExpiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.revokeAuthenticatedSession(created.sessionId);
|
||||||
|
|
||||||
|
expect(sessions.get(created.sessionId)).toMatchObject({
|
||||||
|
id: created.sessionId,
|
||||||
|
status: AccountSessionStatus.REVOKED,
|
||||||
|
refreshTokenHash: null,
|
||||||
|
});
|
||||||
|
expect(sessions.has(created.sessionId)).toBe(true);
|
||||||
|
await expect(
|
||||||
|
service.refreshAuthenticatedSession(
|
||||||
|
created.sessionId,
|
||||||
|
created.refreshToken,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('revokes every account session for one user', async () => {
|
||||||
|
const { service, sessions } = buildService();
|
||||||
|
const first = await service.createAuthenticatedSession(userId, {
|
||||||
|
expiresAt: sessionExpiresAt,
|
||||||
|
});
|
||||||
|
const second = await service.createAuthenticatedSession(userId, {
|
||||||
|
expiresAt: sessionExpiresAt,
|
||||||
|
});
|
||||||
|
const other = await service.createAuthenticatedSession(otherUserId, {
|
||||||
|
expiresAt: sessionExpiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.revokeAllAuthenticatedSessionsForUser(userId),
|
||||||
|
).resolves.toBe(2);
|
||||||
|
|
||||||
|
for (const created of [first, second]) {
|
||||||
|
expect(sessions.get(created.sessionId)).toMatchObject({
|
||||||
|
status: AccountSessionStatus.REVOKED,
|
||||||
|
refreshTokenHash: null,
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
service.refreshAuthenticatedSession(
|
||||||
|
created.sessionId,
|
||||||
|
created.refreshToken,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(UnauthorizedException);
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
service.refreshAuthenticatedSession(
|
||||||
|
other.sessionId,
|
||||||
|
other.refreshToken,
|
||||||
|
),
|
||||||
|
).resolves.toEqual({
|
||||||
|
accessToken: expect.any(String),
|
||||||
|
refreshToken: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates an access token into account authentication context', async () => {
|
||||||
|
const { service } = buildService();
|
||||||
|
const created = await service.createAuthenticatedSession(userId, {
|
||||||
|
expiresAt: sessionExpiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.validateAccessToken(created.accessToken),
|
||||||
|
).resolves.toEqual({
|
||||||
|
userId,
|
||||||
|
sessionId: created.sessionId,
|
||||||
|
accessTokenVersion: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores refresh tokens only as hashes', async () => {
|
||||||
|
const { service, sessions } = buildService();
|
||||||
|
const created = await service.createAuthenticatedSession(userId, {
|
||||||
|
expiresAt: sessionExpiresAt,
|
||||||
|
});
|
||||||
|
const session = sessions.get(created.sessionId);
|
||||||
|
|
||||||
|
expect(session.refreshTokenHash).toBe(
|
||||||
|
createHash('sha256')
|
||||||
|
.update(created.refreshToken, 'utf8')
|
||||||
|
.digest('hex'),
|
||||||
|
);
|
||||||
|
expect(session.refreshTokenHash).not.toContain(created.refreshToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
116
backend/src/modules/auth/authentication-runtime.service.ts
Normal file
116
backend/src/modules/auth/authentication-runtime.service.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AccessTokenService } from './access-token.service';
|
||||||
|
import { RefreshTokenService } from './refresh-token.service';
|
||||||
|
import { SessionService } from './session.service';
|
||||||
|
|
||||||
|
export interface AuthenticationSessionMetadata {
|
||||||
|
expiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticatedSessionTokens {
|
||||||
|
userId: string;
|
||||||
|
sessionId: string;
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
accessTokenExpiresAt: Date;
|
||||||
|
sessionExpiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshedAuthenticatedSessionTokens {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticatedAccountContext {
|
||||||
|
userId: string;
|
||||||
|
sessionId: string;
|
||||||
|
accessTokenVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthenticationRuntimeService {
|
||||||
|
constructor(
|
||||||
|
private readonly sessionService: SessionService,
|
||||||
|
private readonly accessTokenService: AccessTokenService,
|
||||||
|
private readonly refreshTokenService: RefreshTokenService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createAuthenticatedSession(
|
||||||
|
userId: string,
|
||||||
|
metadata: AuthenticationSessionMetadata,
|
||||||
|
): Promise<AuthenticatedSessionTokens> {
|
||||||
|
const refreshToken = await this.refreshTokenService.issueRefreshToken({
|
||||||
|
userId,
|
||||||
|
expiresAt: metadata.expiresAt,
|
||||||
|
});
|
||||||
|
const session =
|
||||||
|
await this.refreshTokenService.verifyRefreshToken(refreshToken);
|
||||||
|
const accessToken = this.accessTokenService.issueAccessToken({
|
||||||
|
userId: session.userId,
|
||||||
|
sessionId: session.id,
|
||||||
|
accessTokenVersion: session.accessTokenVersion,
|
||||||
|
});
|
||||||
|
const accessTokenClaims =
|
||||||
|
this.accessTokenService.decodeAccessToken(accessToken);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: session.userId,
|
||||||
|
sessionId: session.id,
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
accessTokenExpiresAt: new Date(
|
||||||
|
(accessTokenClaims.exp as number) * 1000,
|
||||||
|
),
|
||||||
|
sessionExpiresAt: session.expiresAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshAuthenticatedSession(
|
||||||
|
sessionId: string,
|
||||||
|
refreshToken: string,
|
||||||
|
): Promise<RefreshedAuthenticatedSessionTokens> {
|
||||||
|
await this.refreshTokenService.assertRefreshTokenSession(
|
||||||
|
sessionId,
|
||||||
|
refreshToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextRefreshToken =
|
||||||
|
await this.refreshTokenService.rotateRefreshToken(refreshToken);
|
||||||
|
const session =
|
||||||
|
await this.refreshTokenService.assertRefreshTokenSession(
|
||||||
|
sessionId,
|
||||||
|
nextRefreshToken,
|
||||||
|
);
|
||||||
|
const accessToken = this.accessTokenService.issueAccessToken({
|
||||||
|
userId: session.userId,
|
||||||
|
sessionId: session.id,
|
||||||
|
accessTokenVersion: session.accessTokenVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken: nextRefreshToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeAuthenticatedSession(sessionId: string): Promise<void> {
|
||||||
|
await this.sessionService.revokeSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeAllAuthenticatedSessionsForUser(userId: string): Promise<number> {
|
||||||
|
return this.sessionService.revokeAllSessionsForUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAccessToken(
|
||||||
|
accessToken: string,
|
||||||
|
): Promise<AuthenticatedAccountContext> {
|
||||||
|
const claims =
|
||||||
|
await this.accessTokenService.verifyAccessToken(accessToken);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: claims.sub,
|
||||||
|
sessionId: claims.sid,
|
||||||
|
accessTokenVersion: claims.ver,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -226,6 +226,7 @@ describe('SessionService', () => {
|
|||||||
const result = await service.revokeSession(session.id, now);
|
const result = await service.revokeSession(session.id, now);
|
||||||
|
|
||||||
expect(result.status).toBe(AccountSessionStatus.REVOKED);
|
expect(result.status).toBe(AccountSessionStatus.REVOKED);
|
||||||
|
expect(result.refreshTokenHash).toBeNull();
|
||||||
expect(result.revokedAt).toEqual(now);
|
expect(result.revokedAt).toEqual(now);
|
||||||
await expect(
|
await expect(
|
||||||
service.validateSession(
|
service.validateSession(
|
||||||
@ -248,7 +249,9 @@ describe('SessionService', () => {
|
|||||||
service.revokeAllSessionsForUser(activeUserId, now),
|
service.revokeAllSessionsForUser(activeUserId, now),
|
||||||
).resolves.toBe(2);
|
).resolves.toBe(2);
|
||||||
expect(first.status).toBe(AccountSessionStatus.REVOKED);
|
expect(first.status).toBe(AccountSessionStatus.REVOKED);
|
||||||
|
expect(first.refreshTokenHash).toBeNull();
|
||||||
expect(second.status).toBe(AccountSessionStatus.REVOKED);
|
expect(second.status).toBe(AccountSessionStatus.REVOKED);
|
||||||
|
expect(second.refreshTokenHash).toBeNull();
|
||||||
expect(other.status).toBe(AccountSessionStatus.ACTIVE);
|
expect(other.status).toBe(AccountSessionStatus.ACTIVE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -195,6 +195,7 @@ export class SessionService {
|
|||||||
id: sessionId,
|
id: sessionId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
refreshTokenHash: null,
|
||||||
status: AccountSessionStatus.REVOKED,
|
status: AccountSessionStatus.REVOKED,
|
||||||
revokedAt: now,
|
revokedAt: now,
|
||||||
},
|
},
|
||||||
@ -211,6 +212,7 @@ export class SessionService {
|
|||||||
status: AccountSessionStatus.ACTIVE,
|
status: AccountSessionStatus.ACTIVE,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
refreshTokenHash: null,
|
||||||
status: AccountSessionStatus.REVOKED,
|
status: AccountSessionStatus.REVOKED,
|
||||||
revokedAt: now,
|
revokedAt: now,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user