Host-remote refresh token implementation (#18044)
This commit is contained in:
@@ -19,6 +19,8 @@ export const AuthExceptionCode = appendCommonExceptionCode({
|
||||
FORBIDDEN_EXCEPTION: 'FORBIDDEN_EXCEPTION',
|
||||
INSUFFICIENT_SCOPES: 'INSUFFICIENT_SCOPES',
|
||||
UNAUTHENTICATED: 'UNAUTHENTICATED',
|
||||
APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED:
|
||||
'APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED',
|
||||
INVALID_DATA: 'INVALID_DATA',
|
||||
OAUTH_ACCESS_DENIED: 'OAUTH_ACCESS_DENIED',
|
||||
SSO_AUTH_FAILED: 'SSO_AUTH_FAILED',
|
||||
@@ -56,6 +58,7 @@ const getAuthExceptionUserFriendlyMessage = (
|
||||
case AuthExceptionCode.INSUFFICIENT_SCOPES:
|
||||
return msg`Insufficient permissions.`;
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
case AuthExceptionCode.APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED:
|
||||
return msg`You must be authenticated to perform this action.`;
|
||||
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
|
||||
return msg`OAuth access was denied.`;
|
||||
|
||||
+40
-6
@@ -5,7 +5,10 @@ import { Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationException } from 'src/engine/core-modules/application/application.exception';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
@@ -205,18 +208,49 @@ describe('ApplicationTokenService', () => {
|
||||
expect(() => service.validateApplicationRefreshToken(mockToken)).toThrow(
|
||||
AuthException,
|
||||
);
|
||||
|
||||
try {
|
||||
service.validateApplicationRefreshToken(mockToken);
|
||||
} catch (error) {
|
||||
expect((error as AuthException).code).toBe(
|
||||
AuthExceptionCode.APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw when token verification fails', () => {
|
||||
it('should throw dedicated code when token verification fails', () => {
|
||||
const mockToken = 'invalid-token';
|
||||
|
||||
jest.spyOn(jwtWrapperService, 'verifyJwtToken').mockImplementation(() => {
|
||||
throw new Error('Invalid token');
|
||||
throw new AuthException(
|
||||
'Token has expired.',
|
||||
AuthExceptionCode.UNAUTHENTICATED,
|
||||
);
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
service.validateApplicationRefreshToken(mockToken),
|
||||
).toThrow();
|
||||
expect(() => service.validateApplicationRefreshToken(mockToken)).toThrow(
|
||||
AuthException,
|
||||
);
|
||||
|
||||
try {
|
||||
service.validateApplicationRefreshToken(mockToken);
|
||||
} catch (error) {
|
||||
expect((error as AuthException).code).toBe(
|
||||
AuthExceptionCode.APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should rethrow unexpected token verification errors', () => {
|
||||
const mockToken = 'invalid-token';
|
||||
|
||||
jest.spyOn(jwtWrapperService, 'verifyJwtToken').mockImplementation(() => {
|
||||
throw new Error('Unexpected verification error');
|
||||
});
|
||||
|
||||
expect(() => service.validateApplicationRefreshToken(mockToken)).toThrow(
|
||||
'Unexpected verification error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+30
-13
@@ -27,6 +27,8 @@ import {
|
||||
|
||||
const APPLICATION_ACCESS_TOKEN_EXPIRY_SECONDS = 1800;
|
||||
const APPLICATION_REFRESH_TOKEN_EXPIRY_SECONDS = 60 * 60 * 24 * 60; // 60 days
|
||||
const APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED_MESSAGE =
|
||||
'Application refresh token invalid or expired';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationTokenService {
|
||||
@@ -107,22 +109,37 @@ export class ApplicationTokenService {
|
||||
validateApplicationRefreshToken(
|
||||
refreshToken: string,
|
||||
): ApplicationRefreshTokenJwtPayload {
|
||||
this.jwtWrapperService.verifyJwtToken(refreshToken);
|
||||
try {
|
||||
this.jwtWrapperService.verifyJwtToken(refreshToken);
|
||||
|
||||
const payload =
|
||||
this.jwtWrapperService.decode<ApplicationRefreshTokenJwtPayload>(
|
||||
refreshToken,
|
||||
{ json: true },
|
||||
);
|
||||
const payload =
|
||||
this.jwtWrapperService.decode<ApplicationRefreshTokenJwtPayload>(
|
||||
refreshToken,
|
||||
{ json: true },
|
||||
);
|
||||
|
||||
if (payload.type !== JwtTokenTypeEnum.APPLICATION_REFRESH) {
|
||||
throw new AuthException(
|
||||
'Expected an application refresh token',
|
||||
AuthExceptionCode.INVALID_JWT_TOKEN_TYPE,
|
||||
);
|
||||
if (payload.type !== JwtTokenTypeEnum.APPLICATION_REFRESH) {
|
||||
throw new AuthException(
|
||||
'Expected an application refresh token',
|
||||
AuthExceptionCode.INVALID_JWT_TOKEN_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof AuthException &&
|
||||
(error.code === AuthExceptionCode.UNAUTHENTICATED ||
|
||||
error.code === AuthExceptionCode.INVALID_JWT_TOKEN_TYPE)
|
||||
) {
|
||||
throw new AuthException(
|
||||
APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED_MESSAGE,
|
||||
AuthExceptionCode.APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED,
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async renewApplicationTokens(payload: {
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ export const authGraphqlApiExceptionHandler = (exception: AuthException) => {
|
||||
subCode: exception.code,
|
||||
});
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
case AuthExceptionCode.APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED:
|
||||
throw new AuthenticationError(exception.message, {
|
||||
userFriendlyMessage: msg`You must be authenticated to perform this action.`,
|
||||
subCode: exception.code,
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ export const getAuthExceptionRestStatus = (exception: AuthException) => {
|
||||
case AuthExceptionCode.TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED:
|
||||
case AuthExceptionCode.INVALID_DATA:
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
case AuthExceptionCode.APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED:
|
||||
case AuthExceptionCode.USER_NOT_FOUND:
|
||||
case AuthExceptionCode.WORKSPACE_NOT_FOUND:
|
||||
case AuthExceptionCode.APPLICATION_NOT_FOUND:
|
||||
|
||||
Reference in New Issue
Block a user