Skip to content

Commit

Permalink
Merge branch 'main' into BC-5583-display-pdf-preview
Browse files Browse the repository at this point in the history
  • Loading branch information
bischofmax authored Nov 13, 2023
2 parents 3cf308e + b11b022 commit 44bed1a
Show file tree
Hide file tree
Showing 16 changed files with 24 additions and 1,985 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { SSOAuthenticationError } from '../../interface/sso-authentication-error.enum';

/**
* @deprecated
*/
export class AuthorizationParams {
@IsOptional()
@IsString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { HydraOauthUc } from '@modules/oauth/uc/hydra-oauth.uc';
import { Request } from 'express';
import { OauthSSOController } from './oauth-sso.controller';
import { StatelessAuthorizationParams } from './dto/stateless-authorization.params';
import { OauthUc } from '../uc';

describe('OAuthController', () => {
let module: TestingModule;
Expand Down Expand Up @@ -52,10 +51,6 @@ describe('OAuthController', () => {
provide: LegacyLogger,
useValue: createMock<LegacyLogger>(),
},
{
provide: OauthUc,
useValue: createMock<OauthUc>(),
},
{
provide: HydraOauthUc,
useValue: createMock<HydraOauthUc>(),
Expand Down
174 changes: 8 additions & 166 deletions apps/server/src/modules/oauth/controller/oauth-sso.controller.ts
Original file line number Diff line number Diff line change
@@ -1,150 +1,18 @@
import { Configuration } from '@hpi-schul-cloud/commons/lib';
import {
Controller,
Get,
InternalServerErrorException,
Param,
Query,
Req,
Res,
Session,
UnauthorizedException,
UnprocessableEntityException,
} from '@nestjs/common';
import { ApiOkResponse, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ISession } from '@shared/domain/types/session';
import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication';
import { Controller, Get, Param, Query, Req, UnauthorizedException } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { LegacyLogger } from '@src/core/logger';
import { ICurrentUser, Authenticate, CurrentUser, JWT } from '@modules/authentication';
import { OAuthMigrationError } from '@modules/user-login-migration/error/oauth-migration.error';
import { MigrationDto } from '@modules/user-login-migration/service/dto';
import { CookieOptions, Request, Response } from 'express';
import { HydraOauthUc } from '../uc/hydra-oauth.uc';
import { UserMigrationResponse } from './dto/user-migration.response';
import { OAuthSSOError } from '../loggable/oauth-sso.error';
import { Request } from 'express';
import { OAuthTokenDto } from '../interface';
import { OauthLoginStateMapper } from '../mapper/oauth-login-state.mapper';
import { UserMigrationMapper } from '../mapper/user-migration.mapper';
import { OAuthProcessDto } from '../service/dto';
import { OauthUc } from '../uc';
import { OauthLoginStateDto } from '../uc/dto/oauth-login-state.dto';
import { AuthorizationParams, SSOLoginQuery, SystemIdParams } from './dto';
import { HydraOauthUc } from '../uc';
import { AuthorizationParams } from './dto';
import { StatelessAuthorizationParams } from './dto/stateless-authorization.params';

@ApiTags('SSO')
@Controller('sso')
export class OauthSSOController {
private readonly clientUrl: string;

constructor(
private readonly oauthUc: OauthUc,
private readonly hydraUc: HydraOauthUc,
private readonly logger: LegacyLogger
) {
constructor(private readonly hydraUc: HydraOauthUc, private readonly logger: LegacyLogger) {
this.logger.setContext(OauthSSOController.name);
this.clientUrl = Configuration.get('HOST') as string;
}

private errorHandler(error: unknown, session: ISession, res: Response, provider?: string) {
this.logger.error(error);
const ssoError: OAuthSSOError = error instanceof OAuthSSOError ? error : new OAuthSSOError();

session.destroy((err) => {
this.logger.log(err);
});

const errorRedirect: URL = new URL('/login', this.clientUrl);
errorRedirect.searchParams.append('error', ssoError.errorcode);

if (provider) {
errorRedirect.searchParams.append('provider', provider);
}

res.redirect(errorRedirect.toString());
}

private migrationErrorHandler(error: unknown, session: ISession, res: Response) {
const migrationError: OAuthMigrationError =
error instanceof OAuthMigrationError ? error : new OAuthMigrationError();

session.destroy((err) => {
this.logger.log(err);
});

const errorRedirect: URL = new URL('/migration/error', this.clientUrl);

if (migrationError.officialSchoolNumberFromSource && migrationError.officialSchoolNumberFromTarget) {
errorRedirect.searchParams.append('sourceSchoolNumber', migrationError.officialSchoolNumberFromSource);
errorRedirect.searchParams.append('targetSchoolNumber', migrationError.officialSchoolNumberFromTarget);
}

res.redirect(errorRedirect.toString());
}

private sessionHandler(session: ISession, query: AuthorizationParams): OauthLoginStateDto {
if (!session.oauthLoginState) {
throw new UnauthorizedException('Oauth session not found');
}

const oauthLoginState: OauthLoginStateDto = OauthLoginStateMapper.mapSessionToDto(session);

if (oauthLoginState.state !== query.state) {
throw new UnauthorizedException(`Invalid state. Got: ${query.state} Expected: ${oauthLoginState.state}`);
}

return oauthLoginState;
}

@Get('login/:systemId')
async getAuthenticationUrl(
@Session() session: ISession,
@Res() res: Response,
@Param() params: SystemIdParams,
@Query() query: SSOLoginQuery
): Promise<void> {
try {
const redirect: string = await this.oauthUc.startOauthLogin(
session,
params.systemId,
query.migration || false,
query.postLoginRedirect
);

res.redirect(redirect);
} catch (error) {
this.errorHandler(error, session, res);
}
}

@Get('oauth')
async startOauthAuthorizationCodeFlow(
@Session() session: ISession,
@Res() res: Response,
@Query() query: AuthorizationParams
): Promise<void> {
const oauthLoginState: OauthLoginStateDto = this.sessionHandler(session, query);

try {
const oauthProcessDto: OAuthProcessDto = await this.oauthUc.processOAuthLogin(
oauthLoginState,
query.code,
query.error
);

if (oauthProcessDto.jwt) {
const cookieDefaultOptions: CookieOptions = {
httpOnly: Configuration.get('COOKIE__HTTP_ONLY') as boolean,
sameSite: Configuration.get('COOKIE__SAME_SITE') as 'lax' | 'strict' | 'none',
secure: Configuration.get('COOKIE__SECURE') as boolean,
expires: new Date(Date.now() + (Configuration.get('COOKIE__EXPIRES_SECONDS') as number)),
};

res.cookie('jwt', oauthProcessDto.jwt, cookieDefaultOptions);
}

res.redirect(oauthProcessDto.redirect);
} catch (error) {
this.errorHandler(error, session, res, oauthLoginState.provider);
}
}

@Get('hydra/:oauthClientId')
Expand All @@ -166,7 +34,7 @@ export class OauthSSOController {
): Promise<AuthorizationParams> {
let jwt: string;
const authHeader: string | undefined = req.headers.authorization;
if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
if (authHeader?.toLowerCase()?.startsWith('bearer ')) {
[, jwt] = authHeader.split(' ');
} else {
throw new UnauthorizedException(
Expand All @@ -175,30 +43,4 @@ export class OauthSSOController {
}
return this.hydraUc.requestAuthCode(currentUser.userId, jwt, oauthClientId);
}

@Get('oauth/migration')
@Authenticate('jwt')
@ApiOkResponse({ description: 'The User has been succesfully migrated.' })
@ApiResponse({ type: InternalServerErrorException, description: 'The migration of the User was not possible. ' })
async migrateUser(
@JWT() jwt: string,
@Session() session: ISession,
@CurrentUser() currentUser: ICurrentUser,
@Query() query: AuthorizationParams,
@Res() res: Response
): Promise<void> {
const oauthLoginState: OauthLoginStateDto = this.sessionHandler(session, query);

if (!currentUser.systemId) {
throw new UnprocessableEntityException('Current user does not have a system.');
}

try {
const migration: MigrationDto = await this.oauthUc.migrate(jwt, currentUser.userId, query, oauthLoginState);
const response: UserMigrationResponse = UserMigrationMapper.mapDtoToResponse(migration);
res.redirect(response.redirect);
} catch (error) {
this.migrationErrorHandler(error, session, res);
}
}
}

This file was deleted.

10 changes: 5 additions & 5 deletions apps/server/src/modules/oauth/oauth-api.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Module } from '@nestjs/common';
import { LoggerModule } from '@src/core/logger';
import { AuthenticationModule } from '@modules/authentication/authentication.module';
import { AuthorizationModule } from '@modules/authorization';
import { ProvisioningModule } from '@modules/provisioning';
import { LegacySchoolModule } from '@modules/legacy-school';
import { ProvisioningModule } from '@modules/provisioning';
import { SystemModule } from '@modules/system';
import { UserModule } from '@modules/user';
import { UserLoginMigrationModule } from '@modules/user-login-migration';
import { Module } from '@nestjs/common';
import { LoggerModule } from '@src/core/logger';
import { OauthSSOController } from './controller/oauth-sso.controller';
import { OauthModule } from './oauth.module';
import { HydraOauthUc, OauthUc } from './uc';
import { HydraOauthUc } from './uc';

@Module({
imports: [
Expand All @@ -24,6 +24,6 @@ import { HydraOauthUc, OauthUc } from './uc';
LoggerModule,
],
controllers: [OauthSSOController],
providers: [OauthUc, HydraOauthUc],
providers: [HydraOauthUc],
})
export class OauthApiModule {}
92 changes: 8 additions & 84 deletions apps/server/src/modules/oauth/service/oauth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Configuration } from '@hpi-schul-cloud/commons';
import { Test, TestingModule } from '@nestjs/testing';
import { LegacySchoolDo, OauthConfig, SchoolFeatures, SystemEntity } from '@shared/domain';
import { UserDO } from '@shared/domain/domainobject/user.do';
import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy';
import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption';
import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing';
import { LegacyLogger } from '@src/core/logger';
import { LegacySchoolService } from '@modules/legacy-school';
import { ProvisioningDto, ProvisioningService } from '@modules/provisioning';
import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto';
import { LegacySchoolService } from '@modules/legacy-school';
import { OauthConfigDto } from '@modules/system/service';
import { SystemDto } from '@modules/system/service/dto/system.dto';
import { SystemService } from '@modules/system/service/system.service';
import { UserService } from '@modules/user';
import { MigrationCheckService, UserMigrationService } from '@modules/user-login-migration';
import { Test, TestingModule } from '@nestjs/testing';
import { LegacySchoolDo, OauthConfig, SchoolFeatures, SystemEntity } from '@shared/domain';
import { UserDO } from '@shared/domain/domainobject/user.do';
import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy';
import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing';
import { LegacyLogger } from '@src/core/logger';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { OAuthSSOError, UserNotFoundAfterProvisioningLoggableException } from '../loggable';
import { OAuthTokenDto } from '../interface';
import { OAuthSSOError, UserNotFoundAfterProvisioningLoggableException } from '../loggable';
import { OauthTokenResponse } from './dto';
import { OauthAdapterService } from './oauth-adapter.service';
import { OAuthService } from './oauth.service';
Expand Down Expand Up @@ -560,80 +560,4 @@ describe('OAuthService', () => {
});
});
});

describe('getAuthenticationUrl is called', () => {
describe('when a normal authentication url is requested', () => {
it('should return a authentication url', () => {
const oauthConfig: OauthConfig = new OauthConfig({
clientId: '12345',
clientSecret: 'mocksecret',
tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken',
grantType: 'authorization_code',
redirectUri: 'http://mockhost:3030/api/v3/sso/oauth/testsystemId',
scope: 'openid uuid',
responseType: 'code',
authEndpoint: 'http://mock.de/auth',
provider: 'mock_type',
logoutEndpoint: 'http://mock.de/logout',
issuer: 'mock_issuer',
jwksEndpoint: 'http://mock.de/jwks',
});

const result: string = service.getAuthenticationUrl(oauthConfig, 'state', false);

expect(result).toEqual(
'http://mock.de/auth?client_id=12345&redirect_uri=https%3A%2F%2Fmock.de%2Fapi%2Fv3%2Fsso%2Foauth&response_type=code&scope=openid+uuid&state=state'
);
});
});

describe('when a migration authentication url is requested', () => {
it('should return a authentication url', () => {
const oauthConfig: OauthConfig = new OauthConfig({
clientId: '12345',
clientSecret: 'mocksecret',
tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken',
grantType: 'authorization_code',
redirectUri: 'http://mockhost.de/api/v3/sso/oauth/testsystemId',
scope: 'openid uuid',
responseType: 'code',
authEndpoint: 'http://mock.de/auth',
provider: 'mock_type',
logoutEndpoint: 'http://mock.de/logout',
issuer: 'mock_issuer',
jwksEndpoint: 'http://mock.de/jwks',
});

const result: string = service.getAuthenticationUrl(oauthConfig, 'state', true);

expect(result).toEqual(
'http://mock.de/auth?client_id=12345&redirect_uri=https%3A%2F%2Fmock.de%2Fapi%2Fv3%2Fsso%2Foauth%2Fmigration&response_type=code&scope=openid+uuid&state=state'
);
});

it('should return add an idp hint if existing authentication url', () => {
const oauthConfig: OauthConfig = new OauthConfig({
clientId: '12345',
clientSecret: 'mocksecret',
tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken',
grantType: 'authorization_code',
redirectUri: 'http://mockhost.de/api/v3/sso/oauth/testsystemId',
scope: 'openid uuid',
responseType: 'code',
authEndpoint: 'http://mock.de/auth',
provider: 'mock_type',
logoutEndpoint: 'http://mock.de/logout',
issuer: 'mock_issuer',
jwksEndpoint: 'http://mock.de/jwks',
idpHint: 'TheIdpHint',
});

const result: string = service.getAuthenticationUrl(oauthConfig, 'state', true);

expect(result).toEqual(
'http://mock.de/auth?client_id=12345&redirect_uri=https%3A%2F%2Fmock.de%2Fapi%2Fv3%2Fsso%2Foauth%2Fmigration&response_type=code&scope=openid+uuid&state=state&kc_idp_hint=TheIdpHint'
);
});
});
});
});
Loading

0 comments on commit 44bed1a

Please sign in to comment.