Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N21-1374 removes old login flow #4541

Merged
merged 10 commits into from
Nov 13, 2023

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,
arnegns marked this conversation as resolved.
Show resolved Hide resolved
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
Loading