diff --git a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts index 3c34d4fb303..5ebf33df21f 100644 --- a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts +++ b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts @@ -1,5 +1,4 @@ import { EntityManager } from '@mikro-orm/core'; -import { SSOErrorCode } from '@modules/oauth/loggable'; import { OauthTokenResponse } from '@modules/oauth/service/dto'; import { ServerTestModule } from '@modules/server/server.module'; import { HttpStatus, INestApplication } from '@nestjs/common'; @@ -416,7 +415,7 @@ describe('Login Controller (api)', () => { .post(`${basePath}/oauth2`) .send({ redirectUri: 'redirectUri', - error: SSOErrorCode.SSO_OAUTH_LOGIN_FAILED, + error: 'sso_login_failed', systemId: system.id, }) // TODO N21-820: change this to UNAUTHORIZED when refactoring exceptions diff --git a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts index e98100d3e43..056735c63da 100644 --- a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts +++ b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts @@ -95,7 +95,7 @@ describe('OAuthController', () => { await controller.requestAuthToken(currentUser, request, oauthClientId); - expect(hydraOauthUc.requestAuthCode).toBeCalledWith(currentUser.userId, expect.any(String), oauthClientId); + expect(hydraOauthUc.requestAuthCode).toBeCalledWith(expect.any(String), oauthClientId); }); it('should throw UnauthorizedException', async () => { diff --git a/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts b/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts index 61ed319d1cd..3ea58e99d72 100644 --- a/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts +++ b/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts @@ -41,6 +41,6 @@ export class OauthSSOController { `No bearer token in header for authorization process of user ${currentUser.userId} on oauth system ${oauthClientId}` ); } - return this.hydraUc.requestAuthCode(currentUser.userId, jwt, oauthClientId); + return this.hydraUc.requestAuthCode(jwt, oauthClientId); } } diff --git a/apps/server/src/modules/oauth/loggable/auth-code-failure-loggable-exception.spec.ts b/apps/server/src/modules/oauth/loggable/auth-code-failure-loggable-exception.spec.ts new file mode 100644 index 00000000000..00f2cfa41ae --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/auth-code-failure-loggable-exception.spec.ts @@ -0,0 +1,26 @@ +import { AuthCodeFailureLoggableException } from './auth-code-failure-loggable-exception'; + +describe(AuthCodeFailureLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const errorCode = 'error_code'; + const exception = new AuthCodeFailureLoggableException(errorCode); + return { errorCode, exception }; + }; + + it('should return a LogMessage', () => { + const { errorCode, exception } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toEqual({ + type: 'SSO_AUTH_CODE_STEP', + message: 'Authorization Query Object has no authorization code or error', + stack: exception.stack, + data: { + errorCode, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/oauth/loggable/auth-code-failure-loggable-exception.ts b/apps/server/src/modules/oauth/loggable/auth-code-failure-loggable-exception.ts new file mode 100644 index 00000000000..3798d8d8b73 --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/auth-code-failure-loggable-exception.ts @@ -0,0 +1,19 @@ +import { ErrorLogMessage, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { OauthSsoErrorLoggableException } from './oauth-sso-error-loggable-exception'; + +export class AuthCodeFailureLoggableException extends OauthSsoErrorLoggableException { + constructor(private readonly errorCode?: string) { + super(errorCode ?? 'sso_auth_code_step', 'Authorization Query Object has no authorization code or error'); + } + + override getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'SSO_AUTH_CODE_STEP', + message: 'Authorization Query Object has no authorization code or error', + stack: this.stack, + data: { + errorCode: this.errorCode, + }, + }; + } +} diff --git a/apps/server/src/modules/oauth/loggable/id-token-extraction-failure-loggable-exception.spec.ts b/apps/server/src/modules/oauth/loggable/id-token-extraction-failure-loggable-exception.spec.ts new file mode 100644 index 00000000000..2c0ed9d0e02 --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/id-token-extraction-failure-loggable-exception.spec.ts @@ -0,0 +1,26 @@ +import { IdTokenExtractionFailureLoggableException } from './id-token-extraction-failure-loggable-exception'; + +describe(IdTokenExtractionFailureLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const fieldName = 'id_token'; + const exception = new IdTokenExtractionFailureLoggableException(fieldName); + return { exception, fieldName }; + }; + + it('should return a LogMessage', () => { + const { exception, fieldName } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toEqual({ + type: 'SSO_JWT_PROBLEM', + message: 'Failed to extract field', + stack: exception.stack, + data: { + fieldName, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/oauth/loggable/id-token-extraction-failure-loggable-exception.ts b/apps/server/src/modules/oauth/loggable/id-token-extraction-failure-loggable-exception.ts new file mode 100644 index 00000000000..57c5d354d74 --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/id-token-extraction-failure-loggable-exception.ts @@ -0,0 +1,19 @@ +import { ErrorLogMessage, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { OauthSsoErrorLoggableException } from './oauth-sso-error-loggable-exception'; + +export class IdTokenExtractionFailureLoggableException extends OauthSsoErrorLoggableException { + constructor(private readonly fieldName: string) { + super(); + } + + override getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'SSO_JWT_PROBLEM', + message: 'Failed to extract field', + stack: this.stack, + data: { + fieldName: this.fieldName, + }, + }; + } +} diff --git a/apps/server/src/modules/oauth/loggable/id-token-invalid-loggable-exception.spec.ts b/apps/server/src/modules/oauth/loggable/id-token-invalid-loggable-exception.spec.ts new file mode 100644 index 00000000000..754779962bc --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/id-token-invalid-loggable-exception.spec.ts @@ -0,0 +1,22 @@ +import { IdTokenInvalidLoggableException } from './id-token-invalid-loggable-exception'; + +describe(IdTokenInvalidLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const exception = new IdTokenInvalidLoggableException(); + return { exception }; + }; + + it('should return a LogMessage', () => { + const { exception } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toEqual({ + type: 'SSO_JWT_PROBLEM', + message: 'Failed to validate idToken', + stack: expect.any(String), + }); + }); + }); +}); diff --git a/apps/server/src/modules/oauth/loggable/id-token-invalid-loggable-exception.ts b/apps/server/src/modules/oauth/loggable/id-token-invalid-loggable-exception.ts new file mode 100644 index 00000000000..20a24b28ca1 --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/id-token-invalid-loggable-exception.ts @@ -0,0 +1,12 @@ +import { ErrorLogMessage, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { OauthSsoErrorLoggableException } from './oauth-sso-error-loggable-exception'; + +export class IdTokenInvalidLoggableException extends OauthSsoErrorLoggableException { + override getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'SSO_JWT_PROBLEM', + message: 'Failed to validate idToken', + stack: this.stack, + }; + } +} diff --git a/apps/server/src/modules/oauth/loggable/id-token-user-not-found-loggable-exception.spec.ts b/apps/server/src/modules/oauth/loggable/id-token-user-not-found-loggable-exception.spec.ts new file mode 100644 index 00000000000..65af754db8d --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/id-token-user-not-found-loggable-exception.spec.ts @@ -0,0 +1,34 @@ +import { IdTokenUserNotFoundLoggableException } from './id-token-user-not-found-loggable-exception'; + +describe(IdTokenUserNotFoundLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const uuid = 'uuid'; + const additionalInfo = 'additionalInfo'; + + const exception = new IdTokenUserNotFoundLoggableException(uuid, additionalInfo); + + return { + exception, + uuid, + additionalInfo, + }; + }; + + it('should return a LogMessage', () => { + const { exception, uuid, additionalInfo } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toEqual({ + type: 'SSO_USER_NOTFOUND', + message: 'Failed to find user with uuid from id token', + stack: exception.stack, + data: { + uuid, + additionalInfo, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/oauth/loggable/id-token-user-not-found-loggable-exception.ts b/apps/server/src/modules/oauth/loggable/id-token-user-not-found-loggable-exception.ts new file mode 100644 index 00000000000..70c6f56ea96 --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/id-token-user-not-found-loggable-exception.ts @@ -0,0 +1,20 @@ +import { ErrorLogMessage, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { OauthSsoErrorLoggableException } from './oauth-sso-error-loggable-exception'; + +export class IdTokenUserNotFoundLoggableException extends OauthSsoErrorLoggableException { + constructor(private readonly uuid: string, private readonly additionalInfo?: string) { + super(); + } + + override getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'SSO_USER_NOTFOUND', + message: 'Failed to find user with uuid from id token', + stack: this.stack, + data: { + uuid: this.uuid, + additionalInfo: this.additionalInfo, + }, + }; + } +} diff --git a/apps/server/src/modules/oauth/loggable/index.ts b/apps/server/src/modules/oauth/loggable/index.ts index 4c35983a4ca..ebd14914f4f 100644 --- a/apps/server/src/modules/oauth/loggable/index.ts +++ b/apps/server/src/modules/oauth/loggable/index.ts @@ -1,4 +1,8 @@ -export * from './oauth-sso.error'; -export * from './sso-error-code.enum'; export * from './user-not-found-after-provisioning.loggable-exception'; export * from './token-request-loggable-exception'; +export { OauthSsoErrorLoggableException } from './oauth-sso-error-loggable-exception'; +export { AuthCodeFailureLoggableException } from './auth-code-failure-loggable-exception'; +export { IdTokenInvalidLoggableException } from './id-token-invalid-loggable-exception'; +export { OauthConfigMissingLoggableException } from './oauth-config-missing-loggable-exception'; +export { IdTokenExtractionFailureLoggableException } from './id-token-extraction-failure-loggable-exception'; +export { IdTokenUserNotFoundLoggableException } from './id-token-user-not-found-loggable-exception'; diff --git a/apps/server/src/modules/oauth/loggable/oauth-config-missing-loggable-exception.spec.ts b/apps/server/src/modules/oauth/loggable/oauth-config-missing-loggable-exception.spec.ts new file mode 100644 index 00000000000..9fa1c8c576a --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/oauth-config-missing-loggable-exception.spec.ts @@ -0,0 +1,31 @@ +import { ObjectId } from 'bson'; +import { OauthConfigMissingLoggableException } from './oauth-config-missing-loggable-exception'; + +describe(OauthConfigMissingLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const exception = new OauthConfigMissingLoggableException(systemId); + + return { + exception, + systemId, + }; + }; + + it('should return a LogMessage', () => { + const { exception, systemId } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toEqual({ + type: 'SSO_INTERNAL_ERROR', + message: 'Requested system has no oauth configured', + stack: exception.stack, + data: { + systemId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/oauth/loggable/oauth-config-missing-loggable-exception.ts b/apps/server/src/modules/oauth/loggable/oauth-config-missing-loggable-exception.ts new file mode 100644 index 00000000000..cfff342ff51 --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/oauth-config-missing-loggable-exception.ts @@ -0,0 +1,19 @@ +import { ErrorLogMessage, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { OauthSsoErrorLoggableException } from './oauth-sso-error-loggable-exception'; + +export class OauthConfigMissingLoggableException extends OauthSsoErrorLoggableException { + constructor(private readonly systemId: string) { + super(); + } + + override getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'SSO_INTERNAL_ERROR', + message: 'Requested system has no oauth configured', + stack: this.stack, + data: { + systemId: this.systemId, + }, + }; + } +} diff --git a/apps/server/src/modules/oauth/loggable/oauth-sso-error-loggable-exception.spec.ts b/apps/server/src/modules/oauth/loggable/oauth-sso-error-loggable-exception.spec.ts new file mode 100644 index 00000000000..79776582f25 --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/oauth-sso-error-loggable-exception.spec.ts @@ -0,0 +1,25 @@ +import { OauthSsoErrorLoggableException } from './oauth-sso-error-loggable-exception'; + +describe(OauthSsoErrorLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const exception = new OauthSsoErrorLoggableException(); + + return { + exception, + }; + }; + + it('should return a LogMessage', () => { + const { exception } = setup(); + + const result = exception.getLogMessage(); + + expect(result).toEqual({ + type: 'SSO_LOGIN_FAILED', + message: 'Internal Server Error', + stack: expect.any(String), + }); + }); + }); +}); diff --git a/apps/server/src/modules/oauth/loggable/oauth-sso-error-loggable-exception.ts b/apps/server/src/modules/oauth/loggable/oauth-sso-error-loggable-exception.ts new file mode 100644 index 00000000000..12593554163 --- /dev/null +++ b/apps/server/src/modules/oauth/loggable/oauth-sso-error-loggable-exception.ts @@ -0,0 +1,12 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class OauthSsoErrorLoggableException extends InternalServerErrorException implements Loggable { + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'SSO_LOGIN_FAILED', + message: this.message, + stack: this.stack, + }; + } +} diff --git a/apps/server/src/modules/oauth/loggable/oauth-sso.error.spec.ts b/apps/server/src/modules/oauth/loggable/oauth-sso.error.spec.ts deleted file mode 100644 index 4c68527f19f..00000000000 --- a/apps/server/src/modules/oauth/loggable/oauth-sso.error.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { OAuthSSOError } from './oauth-sso.error'; - -describe('Oauth SSO Error', () => { - it('should be possible to create', () => { - const error = new OAuthSSOError(); - expect(error).toBeDefined(); - expect(error.message).toEqual(error.DEFAULT_MESSAGE); - expect(error.errorcode).toEqual(error.DEFAULT_ERRORCODE); - }); - - it('should be possible to add message', () => { - const msg = 'test message'; - const error = new OAuthSSOError(msg); - expect(error.message).toEqual(msg); - }); - - it('should have the right code', () => { - const errCode = 'test_code'; - const error = new OAuthSSOError('', errCode); - expect(error.errorcode).toEqual(errCode); - }); -}); diff --git a/apps/server/src/modules/oauth/loggable/oauth-sso.error.ts b/apps/server/src/modules/oauth/loggable/oauth-sso.error.ts deleted file mode 100644 index cc1486adcb7..00000000000 --- a/apps/server/src/modules/oauth/loggable/oauth-sso.error.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { InternalServerErrorException } from '@nestjs/common'; -import { SSOErrorCode } from './sso-error-code.enum'; - -/** - * @deprecated Please create a loggable instead. - * This will be removed with: https://ticketsystem.dbildungscloud.de/browse/N21-1483 - */ -export class OAuthSSOError extends InternalServerErrorException { - readonly message: string; - - readonly errorcode: string; - - readonly DEFAULT_MESSAGE: string = 'Error in SSO Oauth Process.'; - - readonly DEFAULT_ERRORCODE: string = SSOErrorCode.SSO_OAUTH_LOGIN_FAILED; - - constructor(message?: string, errorcode?: string) { - super(message); - this.message = message || this.DEFAULT_MESSAGE; - this.errorcode = errorcode || this.DEFAULT_ERRORCODE; - } -} diff --git a/apps/server/src/modules/oauth/loggable/sso-error-code.enum.ts b/apps/server/src/modules/oauth/loggable/sso-error-code.enum.ts deleted file mode 100644 index 2c79746a448..00000000000 --- a/apps/server/src/modules/oauth/loggable/sso-error-code.enum.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum SSOErrorCode { - SSO_INVALID_STATE = 'sso_invalid_state', - SSO_USER_NOT_FOUND = 'sso_user_notfound', - SSO_OAUTH_ACCESS_DENIED = 'sso_oauth_access_denied', - SSO_JWT_PROBLEM = 'sso_jwt_problem', - SSO_OAUTH_INVALID_REQUEST = 'sso_oauth_invalid_request', - SSO_OAUTH_UNSUPPORTED_RESPONSE_TYPE = 'sso_oauth_unsupported_response_type', - SSO_OAUTH_LOGIN_FAILED = 'sso_login_failed', - SSO_AUTH_CODE_STEP = 'sso_auth_code_step', - SSO_INTERNAL_ERROR = 'sso_internal_error', -} diff --git a/apps/server/src/modules/oauth/loggable/user-not-found-after-provisioning.loggable-exception.ts b/apps/server/src/modules/oauth/loggable/user-not-found-after-provisioning.loggable-exception.ts index d5cd050cded..b1f7d243d3f 100644 --- a/apps/server/src/modules/oauth/loggable/user-not-found-after-provisioning.loggable-exception.ts +++ b/apps/server/src/modules/oauth/loggable/user-not-found-after-provisioning.loggable-exception.ts @@ -1,8 +1,8 @@ import { EntityId } from '@shared/domain/types'; import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; -import { OAuthSSOError } from './oauth-sso.error'; +import { OauthSsoErrorLoggableException } from './oauth-sso-error-loggable-exception'; -export class UserNotFoundAfterProvisioningLoggableException extends OAuthSSOError implements Loggable { +export class UserNotFoundAfterProvisioningLoggableException extends OauthSsoErrorLoggableException implements Loggable { constructor( private readonly externalUserId: string, private readonly systemId: EntityId, @@ -14,7 +14,7 @@ export class UserNotFoundAfterProvisioningLoggableException extends OAuthSSOErro ); } - getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + override getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { return { message: this.message, stack: this.stack, diff --git a/apps/server/src/modules/oauth/service/oauth.service.spec.ts b/apps/server/src/modules/oauth/service/oauth.service.spec.ts index 483026b6be8..6ccfd05ffb5 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -15,10 +15,15 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-prov import { legacySchoolDoFactory, setupEntities, systemEntityFactory, userDoFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { OauthDataDto } from '@src/modules/provisioning/dto'; +import { LegacySystemService } from '@src/modules/system'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { LegacySystemService } from '../../system/service/legacy-system.service'; import { OAuthTokenDto } from '../interface'; -import { OAuthSSOError, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; +import { + AuthCodeFailureLoggableException, + IdTokenInvalidLoggableException, + OauthConfigMissingLoggableException, + UserNotFoundAfterProvisioningLoggableException, +} from '../loggable'; import { OauthTokenResponse } from './dto'; import { OauthAdapterService } from './oauth-adapter.service'; import { OAuthService } from './oauth.service'; @@ -186,7 +191,7 @@ describe('OAuthService', () => { jest.spyOn(jwt, 'verify').mockImplementationOnce((): string => 'string'); await expect(service.validateToken('idToken', testOauthConfig)).rejects.toEqual( - new OAuthSSOError('Failed to validate idToken', 'sso_token_verfication_error') + new IdTokenInvalidLoggableException() ); }); }); @@ -257,9 +262,7 @@ describe('OAuthService', () => { const func = () => service.authenticateUser(testSystem.id, 'redirectUri', authCode); - await expect(func).rejects.toThrow( - new OAuthSSOError(`Requested system ${testSystem.id} has no oauth configured`, 'sso_internal_error') - ); + await expect(func).rejects.toThrow(new OauthConfigMissingLoggableException(testSystem.id)); }); }); @@ -267,9 +270,7 @@ describe('OAuthService', () => { it('should throw an error', async () => { const func = () => service.authenticateUser('systemId', 'redirectUri', undefined, 'errorCode'); - await expect(func).rejects.toThrow( - new OAuthSSOError('Authorization Query Object has no authorization code or error', 'errorCode') - ); + await expect(func).rejects.toThrow(new AuthCodeFailureLoggableException('errorCode')); }); }); @@ -277,9 +278,7 @@ describe('OAuthService', () => { it('should throw an error', async () => { const func = () => service.authenticateUser('systemId', 'redirectUri'); - await expect(func).rejects.toThrow( - new OAuthSSOError('Authorization Query Object has no authorization code or error', 'sso_auth_code_step') - ); + await expect(func).rejects.toThrow(new AuthCodeFailureLoggableException()); }); }); }); diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index 340763341bd..48cf88b1435 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -13,7 +13,12 @@ import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; import jwt, { JwtPayload } from 'jsonwebtoken'; import { OAuthTokenDto } from '../interface'; -import { OAuthSSOError, SSOErrorCode, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; +import { + AuthCodeFailureLoggableException, + IdTokenInvalidLoggableException, + OauthConfigMissingLoggableException, + UserNotFoundAfterProvisioningLoggableException, +} from '../loggable'; import { TokenRequestMapper } from '../mapper/token-request.mapper'; import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; import { OauthAdapterService } from './oauth-adapter.service'; @@ -40,15 +45,12 @@ export class OAuthService { errorCode?: string ): Promise { if (errorCode || !authCode) { - throw new OAuthSSOError( - 'Authorization Query Object has no authorization code or error', - errorCode || 'sso_auth_code_step' - ); + throw new AuthCodeFailureLoggableException(errorCode); } const system: SystemDto = await this.systemService.findById(systemId); if (!system.oauthConfig) { - throw new OAuthSSOError(`Requested system ${systemId} has no oauth configured`, 'sso_internal_error'); + throw new OauthConfigMissingLoggableException(systemId); } const { oauthConfig } = system; @@ -141,7 +143,7 @@ export class OAuthService { }); if (typeof decodedJWT === 'string') { - throw new OAuthSSOError('Failed to validate idToken', SSOErrorCode.SSO_JWT_PROBLEM); + throw new IdTokenInvalidLoggableException(); } return decodedJWT; diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts index 8626e969cbc..efaf7190329 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts @@ -13,7 +13,7 @@ import { HydraOauthUc } from '.'; import { AuthorizationParams } from '../controller/dto'; import { StatelessAuthorizationParams } from '../controller/dto/stateless-authorization.params'; import { OAuthTokenDto } from '../interface'; -import { OAuthSSOError } from '../loggable'; +import { AuthCodeFailureLoggableException } from '../loggable'; class HydraOauthUcSpec extends HydraOauthUc { public validateStatusSpec = (status: number) => this.validateStatus(status); @@ -35,7 +35,6 @@ describe('HydraOauthUc', () => { }; const JWTMock = 'jwtMock'; - const userIdMock = 'userIdMock'; const oauthClientId = 'oauthClientIdMock'; const hydraUri = 'hydraUri'; const apiHost = 'apiHost'; @@ -117,9 +116,7 @@ describe('HydraOauthUc', () => { const func = () => uc.getOauthToken('4566456', undefined, error); - await expect(func).rejects.toThrow( - new OAuthSSOError('Authorization Query Object has no authorization code or error', error) - ); + await expect(func).rejects.toThrow(new AuthCodeFailureLoggableException(error)); }); }); @@ -133,9 +130,7 @@ describe('HydraOauthUc', () => { const func = async () => uc.getOauthToken('oauthClientId', undefined, error); - await expect(func).rejects.toThrow( - new OAuthSSOError('Authorization Query Object has no authorization code or error', error) - ); + await expect(func).rejects.toThrow(new AuthCodeFailureLoggableException(error)); }); }); }); @@ -198,7 +193,7 @@ describe('HydraOauthUc', () => { hydraOauthService.processRedirect.mockResolvedValueOnce(responseDto1); hydraOauthService.processRedirect.mockResolvedValueOnce(responseDto2); - const authParams: AuthorizationParams = await uc.requestAuthCode(userIdMock, JWTMock, oauthClientId); + const authParams: AuthorizationParams = await uc.requestAuthCode(JWTMock, oauthClientId); expect(authParams).toStrictEqual(expectedAuthParams); expect(hydraOauthService.processRedirect).toBeCalledTimes(2); @@ -211,9 +206,7 @@ describe('HydraOauthUc', () => { return Promise.resolve(dto); }); - await expect(uc.requestAuthCode(userIdMock, JWTMock, oauthClientId)).rejects.toThrow( - InternalServerErrorException - ); + await expect(uc.requestAuthCode(JWTMock, oauthClientId)).rejects.toThrow(InternalServerErrorException); }); }); diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts index 8a0b3786f0b..7b92362cd57 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts @@ -5,9 +5,8 @@ import { LegacyLogger } from '@src/core/logger'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { AuthorizationParams } from '../controller/dto'; import { OAuthTokenDto } from '../interface'; -import { OAuthSSOError } from '../loggable'; -import { HydraSsoService } from '../service/hydra.service'; -import { OAuthService } from '../service/oauth.service'; +import { AuthCodeFailureLoggableException } from '../loggable'; +import { HydraSsoService, OAuthService } from '../service'; @Injectable() export class HydraOauthUc { @@ -23,10 +22,7 @@ export class HydraOauthUc { async getOauthToken(oauthClientId: string, code?: string, error?: string): Promise { if (error || !code) { - throw new OAuthSSOError( - 'Authorization Query Object has no authorization code or error', - error || 'sso_auth_code_step' - ); + throw new AuthCodeFailureLoggableException(error); } const hydraOauthConfig: OauthConfigEntity = await this.hydraSsoService.generateConfig(oauthClientId); @@ -43,7 +39,7 @@ export class HydraOauthUc { protected validateStatus = (status: number): boolean => status === 200 || status === 302; - async requestAuthCode(userId: string, jwt: string, oauthClientId: string): Promise { + async requestAuthCode(jwt: string, oauthClientId: string): Promise { const hydraOauthConfig: OauthConfigEntity = await this.hydraSsoService.generateConfig(oauthClientId); const axiosConfig: AxiosRequestConfig = { headers: {}, diff --git a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts index 6e1e909a4ba..22f702f2b1d 100644 --- a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts @@ -1,7 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { LegacySchoolService } from '@modules/legacy-school'; -import { OAuthSSOError } from '@modules/oauth/loggable'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, UserDO } from '@shared/domain/domainobject'; @@ -10,6 +9,10 @@ import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { legacySchoolDoFactory, schoolFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; import jwt from 'jsonwebtoken'; +import { + IdTokenExtractionFailureLoggableException, + IdTokenUserNotFoundLoggableException, +} from '@src/modules/oauth/loggable'; import { RoleDto } from '../../../role/service/dto/role.dto'; import { ExternalSchoolDto, @@ -130,7 +133,7 @@ describe('IservProvisioningStrategy', () => { const func = () => strategy.getData(input); - await expect(func).rejects.toThrow(new OAuthSSOError('Failed to extract uuid', 'sso_jwt_problem')); + await expect(func).rejects.toThrow(new IdTokenExtractionFailureLoggableException('uuid')); }); }); @@ -152,10 +155,7 @@ describe('IservProvisioningStrategy', () => { const func = () => strategy.getData(input); await expect(func).rejects.toThrow( - new OAuthSSOError( - `Failed to find user with Id ${userUUID} [schoolId: ${schoolId}, currentLdapId: ${userUUID}]`, - 'sso_user_notfound' - ) + new IdTokenUserNotFoundLoggableException(userUUID, `email: ${email}, schoolId: ${schoolId}`) ); }); @@ -170,9 +170,7 @@ describe('IservProvisioningStrategy', () => { const func = () => strategy.getData(input); - await expect(func).rejects.toThrow( - new OAuthSSOError(`Failed to find user with Id ${userUUID}`, 'sso_user_notfound') - ); + await expect(func).rejects.toThrow(new IdTokenUserNotFoundLoggableException(userUUID)); }); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts index 4803c80e297..9567bcb4664 100644 --- a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts @@ -1,5 +1,4 @@ import { LegacySchoolService } from '@modules/legacy-school'; -import { OAuthSSOError } from '@modules/oauth/loggable'; import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { LegacySchoolDo, RoleReference, UserDO } from '@shared/domain/domainobject'; @@ -7,6 +6,10 @@ import { User } from '@shared/domain/entity'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import jwt, { JwtPayload } from 'jsonwebtoken'; +import { + IdTokenExtractionFailureLoggableException, + IdTokenUserNotFoundLoggableException, +} from '@modules/oauth/loggable'; import { ExternalSchoolDto, ExternalUserDto, @@ -31,7 +34,7 @@ export class IservProvisioningStrategy extends ProvisioningStrategy { const idToken: JwtPayload | null = jwt.decode(input.idToken, { json: true }); if (!idToken || !idToken.uuid) { - throw new OAuthSSOError('Failed to extract uuid', 'sso_jwt_problem'); + throw new IdTokenExtractionFailureLoggableException('uuid'); } const ldapUser: UserDO | null = await this.userService.findByExternalId( @@ -40,10 +43,7 @@ export class IservProvisioningStrategy extends ProvisioningStrategy { ); if (!ldapUser) { const additionalInfo: string = await this.getAdditionalErrorInfo(idToken.email as string | undefined); - throw new OAuthSSOError( - `Failed to find user with Id ${idToken.uuid as string}${additionalInfo}`, - 'sso_user_notfound' - ); + throw new IdTokenUserNotFoundLoggableException(idToken?.uuid as string, additionalInfo); } const ldapSchool: LegacySchoolDo = await this.schoolService.getSchoolById(ldapUser.schoolId); diff --git a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts index f9235522100..5c0c8901077 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import jwt from 'jsonwebtoken'; -import { OAuthSSOError } from '@modules/oauth/loggable'; +import { IdTokenExtractionFailureLoggableException } from '@src/modules/oauth/loggable'; import { ExternalUserDto, OauthDataDto, @@ -83,7 +83,7 @@ describe('OidcMockProvisioningStrategy', () => { const result: Promise = strategy.getData(input); - await expect(result).rejects.toThrow(OAuthSSOError); + await expect(result).rejects.toThrow(new IdTokenExtractionFailureLoggableException('external_sub')); }); it('should throw error when there is no idToken', async () => { @@ -92,7 +92,7 @@ describe('OidcMockProvisioningStrategy', () => { const result: Promise = strategy.getData(input); - await expect(result).rejects.toThrow(OAuthSSOError); + await expect(result).rejects.toThrow(new IdTokenExtractionFailureLoggableException('external_sub')); }); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts index e505386ca97..dd15672c3c9 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { OAuthSSOError } from '@modules/oauth/loggable'; +import { IdTokenExtractionFailureLoggableException } from '@modules/oauth/loggable'; import { ExternalUserDto, OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto } from '../../dto'; import { ProvisioningStrategy } from '../base.strategy'; @@ -14,7 +14,7 @@ export class OidcMockProvisioningStrategy extends ProvisioningStrategy { override async getData(input: OauthDataStrategyInputDto): Promise { const idToken = jwt.decode(input.idToken, { json: true }) as (JwtPayload & { external_sub?: string }) | null; if (!idToken || !idToken.external_sub) { - throw new OAuthSSOError('Failed to extract external_sub', 'sso_jwt_problem'); + throw new IdTokenExtractionFailureLoggableException('external_sub'); } const externalUser: ExternalUserDto = new ExternalUserDto({