diff --git a/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 b/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 index 70645a1c4a5..53f76e967b2 100644 --- a/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/admin-api-server-configmap.yml.j2 @@ -22,3 +22,4 @@ data: IDENTITY_MANAGEMENT__TENANT: "{{ IDENTITY_MANAGEMENT__TENANT }}" IDENTITY_MANAGEMENT__CLIENTID: "{{ IDENTITY_MANAGEMENT__CLIENTID }}" TLDRAW__WEBSOCKET_URL: "wss://{{ DOMAIN }}/tldraw-server" + JWT_PUBLIC_KEY: "{{ JWT_PUBLIC_KEY }}" diff --git a/ansible/roles/schulcloud-server-core/templates/preview-generator-configmap.yml.j2 b/ansible/roles/schulcloud-server-core/templates/preview-generator-configmap.yml.j2 index 7ef3e22d623..3fd1be78ae0 100644 --- a/ansible/roles/schulcloud-server-core/templates/preview-generator-configmap.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/preview-generator-configmap.yml.j2 @@ -8,3 +8,5 @@ metadata: data: NEST_LOG_LEVEL: "{{ NEST_LOG_LEVEL }}" EXIT_ON_ERROR: "true" + SC_DOMAIN: "{{ DOMAIN }}" + JWT_PUBLIC_KEY: "{{ JWT_PUBLIC_KEY }}" diff --git a/apps/server/src/infra/auth-guard/auth-guard.config.ts b/apps/server/src/infra/auth-guard/auth-guard.config.ts index 4b6fc411410..a31c148cb48 100644 --- a/apps/server/src/infra/auth-guard/auth-guard.config.ts +++ b/apps/server/src/infra/auth-guard/auth-guard.config.ts @@ -1,6 +1,8 @@ +import { Algorithm } from 'jsonwebtoken'; + export interface AuthGuardConfig { ADMIN_API__ALLOWED_API_KEYS: string[]; - JWT_AUD: string; - JWT_LIFETIME: string; - AUTHENTICATION: string; + JWT_PUBLIC_KEY: string; + JWT_SIGNING_ALGORITHM: Algorithm; + SC_DOMAIN: string; } diff --git a/apps/server/src/infra/auth-guard/config/auth-config.ts b/apps/server/src/infra/auth-guard/config/auth-config.ts deleted file mode 100644 index 02dc8c6fdfa..00000000000 --- a/apps/server/src/infra/auth-guard/config/auth-config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Configuration } from '@hpi-schul-cloud/commons'; -import { AuthConfigFactory } from '../mapper'; - -const jwtOptions = { - audience: Configuration.get('JWT_AUD') as string, - issuer: 'feathers', // should be in our case need to be changed in part 3 of this ticket Configuration.get('JWT_AUD') as string, - expiresIn: Configuration.get('JWT_LIFETIME') as string, -}; - -export const authConfig = AuthConfigFactory.build(Configuration.get('AUTHENTICATION'), jwtOptions); diff --git a/apps/server/src/infra/auth-guard/config/index.ts b/apps/server/src/infra/auth-guard/config/index.ts index 80219784f77..cd9c0d28b96 100644 --- a/apps/server/src/infra/auth-guard/config/index.ts +++ b/apps/server/src/infra/auth-guard/config/index.ts @@ -1,2 +1 @@ -export * from './auth-config'; export * from './x-api-key.config'; diff --git a/apps/server/src/infra/auth-guard/index.ts b/apps/server/src/infra/auth-guard/index.ts index 53ca333e40d..92fa2f36347 100644 --- a/apps/server/src/infra/auth-guard/index.ts +++ b/apps/server/src/infra/auth-guard/index.ts @@ -1,7 +1,7 @@ export { JwtValidationAdapter } from './adapter'; export { AuthGuardModule } from './auth-guard.module'; export { AuthGuardConfig } from './auth-guard.config'; -export { XApiKeyConfig, authConfig } from './config'; +export { XApiKeyConfig } from './config'; export { CurrentUser, JWT, JwtAuthentication } from './decorator'; // JwtAuthGuard only exported because api tests still overried this guard. // Use JwtAuthentication decorator for request validation diff --git a/apps/server/src/infra/auth-guard/mapper/authConfig.factory.spec.ts b/apps/server/src/infra/auth-guard/mapper/authConfig.factory.spec.ts deleted file mode 100644 index 1c9caf86b37..00000000000 --- a/apps/server/src/infra/auth-guard/mapper/authConfig.factory.spec.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { Algorithms, AuthConfigFactory, JwtConstants } from './authConfig.factory'; - -const buildNotAStringError = () => new Error(`Type is not a string`); -const buildNotAnObjectError = () => new Error(`Type is not an object.`); - -describe('AuthConfigFactory.build', () => { - describe('when input is valid', () => { - const setup = () => { - const secret = 'mysecret'; - const input = { - audience: 'myaudience', - issuer: 'myissuer', - expiresIn: '1h', - }; - const expectedResult: JwtConstants = { - secret: 'mysecret', - jwtOptions: { - header: { typ: 'access' }, - audience: 'myaudience', - issuer: 'myissuer', - algorithm: Algorithms.HS256, - expiresIn: '1h', - }, - }; - - return { secret, input, expectedResult }; - }; - - it('should map input to JwtConstants', () => { - const { secret, input, expectedResult } = setup(); - - const result: JwtConstants = AuthConfigFactory.build(secret, input); - - expect(result).toEqual(expectedResult); - }); - }); - - describe('when input is null', () => { - const setup = () => { - const secret = 'mysecret'; - const input = null; - const error = buildNotAnObjectError(); - - return { secret, input, error }; - }; - - it('should throw', () => { - const { secret, input, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, input)).toThrow(error); - }); - }); - - describe('when input is undefined', () => { - const setup = () => { - const secret = 'mysecret'; - const input = undefined; - const error = buildNotAnObjectError(); - - return { secret, input, error }; - }; - - it('should throw', () => { - const { secret, input, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, input)).toThrow(error); - }); - }); - - describe('when secret prop is unedfined', () => { - const setup = () => { - const secret = undefined; - const jwtOptions = { - audience: 'myaudience', - issuer: 'myissuer', - expiresIn: '1h', - }; - const error = buildNotAStringError(); - - return { secret, jwtOptions, error }; - }; - - it('should throw', () => { - const { secret, jwtOptions, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, jwtOptions)).toThrow(error); - }); - }); - - describe('when secret prop is number', () => { - const setup = () => { - const secret = 123; - const jwtOptions = { - audience: 'myaudience', - issuer: 'myissuer', - expiresIn: '1h', - }; - const error = buildNotAStringError(); - - return { secret, jwtOptions, error }; - }; - - it('should throw', () => { - const { secret, jwtOptions, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, jwtOptions)).toThrow(error); - }); - }); - - describe('when jwtOptions is unedfined', () => { - const setup = () => { - const secret = 'mysecret'; - const jwtOptions = undefined; - const error = buildNotAnObjectError(); - - return { secret, jwtOptions, error }; - }; - - it('should throw', () => { - const { secret, jwtOptions, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, jwtOptions)).toThrow(error); - }); - }); - - describe('when audience prop is undefined', () => { - const setup = () => { - const secret = 'mysecret'; - const jwtOptions = { - issuer: 'myissuer', - expiresIn: '1h', - }; - const error = new Error( - 'Object has missing key. Required are: ["audience","issuer","expiresIn"]. Get object keys: ["issuer","expiresIn"]' - ); - - return { secret, jwtOptions, error }; - }; - - it('should throw', () => { - const { secret, jwtOptions, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, jwtOptions)).toThrow(error); - }); - }); - - describe('when audience prop is number', () => { - const setup = () => { - const secret = 'mysecret'; - const jwtOptions = { - audience: 123, - issuer: 'myissuer', - expiresIn: '1h', - }; - const error = buildNotAStringError(); - - return { secret, jwtOptions, error }; - }; - - it('should throw', () => { - const { secret, jwtOptions, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, jwtOptions)).toThrow(error); - }); - }); - - describe('when issuer prop is undefined', () => { - const setup = () => { - const secret = 'mysecret'; - const jwtOptions = { - audience: 'myaudience', - issuer: undefined, - expiresIn: '1h', - }; - const error = buildNotAStringError(); - - return { secret, jwtOptions, error }; - }; - - it('should throw', () => { - const { secret, jwtOptions, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, jwtOptions)).toThrow(error); - }); - }); - - describe('when issuer prop is number', () => { - const setup = () => { - const secret = 'mysecret'; - const jwtOptions = { - audience: 'myaudience', - issuer: 123, - expiresIn: '1h', - }; - const error = buildNotAStringError(); - - return { secret, jwtOptions, error }; - }; - - it('should throw', () => { - const { secret, jwtOptions, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, jwtOptions)).toThrow(error); - }); - }); - - describe('when expiresIn prop is undefined', () => { - const setup = () => { - const secret = 'mysecret'; - const jwtOptions = { - audience: 'myaudience', - issuer: 'myissuer', - }; - const error = new Error( - 'Object has missing key. Required are: ["audience","issuer","expiresIn"]. Get object keys: ["audience","issuer"]' - ); - - return { secret, jwtOptions, error }; - }; - - it('should throw', () => { - const { secret, jwtOptions, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, jwtOptions)).toThrow(error); - }); - }); - - describe('when expiresIn prop is number', () => { - const setup = () => { - const secret = 'mysecret'; - const jwtOptions = { - audience: 'myaudience', - issuer: 'myissuer', - expiresIn: 123, - }; - const error = buildNotAStringError(); - - return { secret, jwtOptions, error }; - }; - - it('should throw', () => { - const { secret, jwtOptions, error } = setup(); - - expect(() => AuthConfigFactory.build(secret, jwtOptions)).toThrow(error); - }); - }); -}); diff --git a/apps/server/src/infra/auth-guard/mapper/authConfig.factory.ts b/apps/server/src/infra/auth-guard/mapper/authConfig.factory.ts deleted file mode 100644 index 7c9f3d84aba..00000000000 --- a/apps/server/src/infra/auth-guard/mapper/authConfig.factory.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { TypeGuard } from '@shared/common'; - -export enum Algorithms { - HS256 = 'HS256', - HS384 = 'HS384', - HS512 = 'HS512', - RS256 = 'RS256', - RS384 = 'RS384', - RS512 = 'RS512', - ES256 = 'ES256', - ES384 = 'ES384', - ES512 = 'ES512', - PS256 = 'PS256', - PS384 = 'PS384', - PS512 = 'PS512', - none = 'none', -} - -/* - TODO: look at existing keys, vs implemented keys - support: true, - supportUserId, - accountId, - userId, - iat, - exp, - aud: this.aud, - iss: 'feathers', - sub: accountId, - jti: `support_${ObjectId()}`, -*/ - -interface JwtOptionsHeader { - typ: string; - alg?: Algorithms; -} - -export interface JwtOptions { - header: JwtOptionsHeader; - audience: string; - issuer: string; - algorithm: Algorithms; - expiresIn: string; - // keys are missing! -} - -export interface JwtConstants { - secret: string; - jwtOptions: JwtOptions; -} - -// Check if it not more a jwt factory and should be renamed and moved -export class AuthConfigFactory { - public static build(secretInput: unknown, jwtOptionsInput: unknown): JwtConstants { - const secret = TypeGuard.checkString(secretInput); - // Should we add length check for secrets and that it is NOT secrets like for local? - - const jwtOptions = TypeGuard.checkKeysInObject(jwtOptionsInput, ['audience', 'issuer', 'expiresIn']); - const audience = TypeGuard.checkString(jwtOptions.audience); - const issuer = TypeGuard.checkString(jwtOptions.issuer); - const expiresIn = TypeGuard.checkString(jwtOptions.expiresIn); - - const jwtConstants = { - secret, - jwtOptions: { - header: { typ: 'access' }, // or should it be typ: 'JWT' ? alg ? - audience, - issuer, - algorithm: Algorithms.HS256, - expiresIn, - }, - }; - - return jwtConstants; - } -} diff --git a/apps/server/src/infra/auth-guard/mapper/index.ts b/apps/server/src/infra/auth-guard/mapper/index.ts index 94ae9f21978..4832027d67a 100644 --- a/apps/server/src/infra/auth-guard/mapper/index.ts +++ b/apps/server/src/infra/auth-guard/mapper/index.ts @@ -1,3 +1,3 @@ -export * from './authConfig.factory'; export * from './current-user.factory'; export * from './jwt.factory'; +export * from './jwt-strategy-options.factory'; diff --git a/apps/server/src/infra/auth-guard/mapper/jwt-strategy-options.factory.ts b/apps/server/src/infra/auth-guard/mapper/jwt-strategy-options.factory.ts new file mode 100644 index 00000000000..6e9e5f67fd0 --- /dev/null +++ b/apps/server/src/infra/auth-guard/mapper/jwt-strategy-options.factory.ts @@ -0,0 +1,24 @@ +import { ConfigService } from '@nestjs/config'; +import { JwtFromRequestFunction, StrategyOptions } from 'passport-jwt'; +import { AuthGuardConfig } from '../auth-guard.config'; + +export class JwtStrategyOptionsFactory { + static build( + jwtFromRequestFunction: JwtFromRequestFunction, + configService: ConfigService + ): StrategyOptions { + const publicKey = configService.getOrThrow('JWT_PUBLIC_KEY'); + const algorithm = configService.getOrThrow('JWT_SIGNING_ALGORITHM'); + + const options = { + jwtFromRequest: jwtFromRequestFunction, + secretOrKey: publicKey, + ignoreExpiration: false, + algorithms: [algorithm], + issuer: configService.getOrThrow('SC_DOMAIN'), + audience: configService.getOrThrow('SC_DOMAIN'), + }; + + return options; + } +} diff --git a/apps/server/src/infra/auth-guard/strategy/jwt.strategy.spec.ts b/apps/server/src/infra/auth-guard/strategy/jwt.strategy.spec.ts index eff14de3ca4..3b142b3e0cd 100644 --- a/apps/server/src/infra/auth-guard/strategy/jwt.strategy.spec.ts +++ b/apps/server/src/infra/auth-guard/strategy/jwt.strategy.spec.ts @@ -1,60 +1,28 @@ -import { JwtModule } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; -import { Test, TestingModule } from '@nestjs/testing'; - import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnauthorizedException } from '@nestjs/common'; -import { jwtPayloadFactory, setupEntities } from '@shared/testing'; - +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { jwtPayloadFactory } from '@shared/testing'; import { JwtValidationAdapter } from '../adapter/jwt-validation.adapter'; import { JwtStrategy } from './jwt.strategy'; -jest.mock('../config', () => { - const authConfig = { - secret: 'mysecret', - jwtOptions: { - header: { typ: 'JWT' }, - audience: 'myaudience', - issuer: 'myissuer', - algorithm: 'HS256', - expiresIn: '1h', - }, - }; - - return { - authConfig, - }; -}); - -const buildAuthConfig = () => { - return { - secret: 'mysecret', - jwtOptions: { - header: { typ: 'JWT' }, - audience: 'myaudience', - issuer: 'myissuer', - algorithm: 'HS256', - expiresIn: '1h', - }, - }; -}; - describe('jwt strategy', () => { let validationAdapter: DeepMocked; let strategy: JwtStrategy; let module: TestingModule; beforeAll(async () => { - await setupEntities(); - module = await Test.createTestingModule({ - imports: [PassportModule, JwtModule.register(buildAuthConfig())], providers: [ JwtStrategy, { provide: JwtValidationAdapter, useValue: createMock(), }, + { + provide: ConfigService, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/infra/auth-guard/strategy/jwt.strategy.ts b/apps/server/src/infra/auth-guard/strategy/jwt.strategy.ts index 4722ab94c5e..4579b29115b 100644 --- a/apps/server/src/infra/auth-guard/strategy/jwt.strategy.ts +++ b/apps/server/src/infra/auth-guard/strategy/jwt.strategy.ts @@ -1,21 +1,22 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { extractJwtFromHeader } from '@shared/common'; import { Strategy } from 'passport-jwt'; import { JwtValidationAdapter } from '../adapter'; -import { authConfig } from '../config'; +import { AuthGuardConfig } from '../auth-guard.config'; import { ICurrentUser, JwtPayload } from '../interface'; -import { CurrentUserBuilder } from '../mapper'; +import { CurrentUserBuilder, JwtStrategyOptionsFactory } from '../mapper'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(private readonly jwtValidationAdapter: JwtValidationAdapter) { - super({ - jwtFromRequest: extractJwtFromHeader, - ignoreExpiration: false, - secretOrKey: authConfig.secret, - ...authConfig.jwtOptions, - }); + constructor( + private readonly jwtValidationAdapter: JwtValidationAdapter, + configService: ConfigService + ) { + const strategyOptions = JwtStrategyOptionsFactory.build(extractJwtFromHeader, configService); + + super(strategyOptions); } async validate(payload: JwtPayload): Promise { diff --git a/apps/server/src/infra/auth-guard/strategy/ws-jwt.strategy.spec.ts b/apps/server/src/infra/auth-guard/strategy/ws-jwt.strategy.spec.ts index 685710a922b..5b5ecf068a1 100644 --- a/apps/server/src/infra/auth-guard/strategy/ws-jwt.strategy.spec.ts +++ b/apps/server/src/infra/auth-guard/strategy/ws-jwt.strategy.spec.ts @@ -1,58 +1,28 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { JwtModule } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { WsException } from '@nestjs/websockets'; -import { jwtPayloadFactory, setupEntities } from '@shared/testing'; +import { jwtPayloadFactory } from '@shared/testing'; import { JwtValidationAdapter } from '../adapter'; import { WsJwtStrategy } from './ws-jwt.strategy'; -jest.mock('../config', () => { - const authConfig = { - secret: 'mysecret', - jwtOptions: { - header: { typ: 'JWT' }, - audience: 'myaudience', - issuer: 'myissuer', - algorithm: 'HS256', - expiresIn: '1h', - }, - }; - - return { - authConfig, - }; -}); - -const buildAuthConfig = () => { - return { - secret: 'mysecret', - jwtOptions: { - header: { typ: 'JWT' }, - audience: 'myaudience', - issuer: 'myissuer', - algorithm: 'HS256', - expiresIn: '1h', - }, - }; -}; - describe('jwt strategy', () => { let validationAdapter: DeepMocked; let strategy: WsJwtStrategy; let module: TestingModule; beforeAll(async () => { - await setupEntities(); - module = await Test.createTestingModule({ - imports: [PassportModule, JwtModule.register(buildAuthConfig())], providers: [ WsJwtStrategy, { provide: JwtValidationAdapter, useValue: createMock(), }, + { + provide: ConfigService, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/infra/auth-guard/strategy/ws-jwt.strategy.ts b/apps/server/src/infra/auth-guard/strategy/ws-jwt.strategy.ts index 4136e9616fe..46b54cb395d 100644 --- a/apps/server/src/infra/auth-guard/strategy/ws-jwt.strategy.ts +++ b/apps/server/src/infra/auth-guard/strategy/ws-jwt.strategy.ts @@ -1,22 +1,23 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { WsException } from '@nestjs/websockets'; import { JwtExtractor } from '@shared/common'; -import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Strategy } from 'passport-jwt'; import { JwtValidationAdapter } from '../adapter'; -import { authConfig } from '../config'; +import { AuthGuardConfig } from '../auth-guard.config'; import { ICurrentUser, JwtPayload, StrategyType } from '../interface'; -import { CurrentUserBuilder } from '../mapper'; +import { CurrentUserBuilder, JwtStrategyOptionsFactory } from '../mapper'; @Injectable() export class WsJwtStrategy extends PassportStrategy(Strategy, StrategyType.WS_JWT) { - constructor(private readonly jwtValidationAdapter: JwtValidationAdapter) { - super({ - jwtFromRequest: ExtractJwt.fromExtractors([JwtExtractor.fromCookie('jwt')]), - ignoreExpiration: false, - secretOrKey: authConfig.secret, - ...authConfig.jwtOptions, - }); + constructor( + private readonly jwtValidationAdapter: JwtValidationAdapter, + configService: ConfigService + ) { + const strategyOptions = JwtStrategyOptionsFactory.build(JwtExtractor.fromCookie('jwt'), configService); + + super(strategyOptions); } async validate(payload: JwtPayload): Promise { diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.integration.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.integration.spec.ts index 90d798ea650..2db181b930b 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.integration.spec.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.integration.spec.ts @@ -7,7 +7,9 @@ import { TspClientModule } from './tsp-client.module'; // NOTE: This test is skipped because it requires a valid client id, secret and token endpoint. // It is meant to be used for manual testing only. -describe('TspClientFactory Integration', () => { +// This test expects that configService.getOrThrow is only used for the specified keys. This is not a reasonable expectation. +// In fact getOrThrow is used now at another place and the test is broken. +describe.skip('TspClientFactory Integration', () => { let module: TestingModule; let sut: TspClientFactory; diff --git a/apps/server/src/modules/authentication/authentication-api-test.module.ts b/apps/server/src/modules/authentication/authentication-api-test.module.ts new file mode 100644 index 00000000000..a83a4f8825c --- /dev/null +++ b/apps/server/src/modules/authentication/authentication-api-test.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AuthenticationTestModule } from './authentication-test.module'; +import { LoginController, LogoutController } from './controllers'; +import { LoginUc, LogoutUc } from './uc'; + +// This module is for use in api tests of other apps than the core server. +@Module({ + imports: [AuthenticationTestModule], + providers: [LoginUc, LogoutUc], + controllers: [LoginController, LogoutController], +}) +export class AuthenticationApiTestModule {} diff --git a/apps/server/src/modules/authentication/authentication-config.ts b/apps/server/src/modules/authentication/authentication-config.ts index d86aa80d822..898a1802e7b 100644 --- a/apps/server/src/modules/authentication/authentication-config.ts +++ b/apps/server/src/modules/authentication/authentication-config.ts @@ -1,13 +1,17 @@ import { XApiKeyConfig } from '@infra/auth-guard'; import { AccountConfig } from '@modules/account'; +import { Algorithm } from 'jsonwebtoken'; export interface AuthenticationConfig extends AccountConfig, XApiKeyConfig { - AUTHENTICATION: string; DISABLED_BRUTE_FORCE_CHECK: boolean; FEATURE_JWT_EXTENDED_TIMEOUT_ENABLED: boolean; + JWT_PRIVATE_KEY: string; + JWT_PUBLIC_KEY: string; + JWT_SIGNING_ALGORITHM: Algorithm; JWT_LIFETIME: string; JWT_TIMEOUT_SECONDS: number; JWT_LIFETIME_SUPPORT_SECONDS: number; JWT_EXTENDED_TIMEOUT_SECONDS: number; + SC_DOMAIN: string; LOGIN_BLOCK_TIME: number; } diff --git a/apps/server/src/modules/authentication/authentication-test.module.ts b/apps/server/src/modules/authentication/authentication-test.module.ts new file mode 100644 index 00000000000..11c9cd1380d --- /dev/null +++ b/apps/server/src/modules/authentication/authentication-test.module.ts @@ -0,0 +1,75 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { AuthGuardModule } from '@infra/auth-guard'; +import { CacheWrapperModule } from '@infra/cache'; +import { IdentityManagementModule } from '@infra/identity-management'; +import { AccountModule } from '@modules/account'; +import { OauthModule } from '@modules/oauth/oauth.module'; +import { RoleModule } from '@modules/role'; +import { SystemModule } from '@modules/system'; +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { LegacySchoolRepo, UserRepo } from '@shared/repo'; +import { LoggerModule } from '@src/core/logger'; +import { Algorithm, SignOptions } from 'jsonwebtoken'; +import { JwtWhitelistAdapter } from './helper/jwt-whitelist.adapter'; +import { AuthenticationService } from './services/authentication.service'; +import { LdapService } from './services/ldap.service'; +import { LdapStrategy } from './strategy/ldap.strategy'; +import { LocalStrategy } from './strategy/local.strategy'; +import { Oauth2Strategy } from './strategy/oauth2.strategy'; + +const createJwtOptions = () => { + const algorithm = Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm; + + const signOptions: SignOptions = { + algorithm, + expiresIn: Configuration.get('JWT_LIFETIME') as string, + issuer: Configuration.get('SC_DOMAIN') as string, + audience: Configuration.get('SC_DOMAIN') as string, + header: { typ: 'JWT', alg: algorithm }, + }; + + const privateKey = Configuration.get('JWT_PRIVATE_KEY') as string; + const publicKey = Configuration.get('JWT_PUBLIC_KEY') as string; + + const options = { + privateKey, + publicKey, + signOptions, + verifyOptions: signOptions, + }; + + return options; +}; + +// This module is for use by the AuthenticationApiTestModule, i.e. for use in api tests of other apps than the core server. +// In those tests the configService can't be used, because the core server's config is not available. +@Module({ + imports: [ + LoggerModule, + PassportModule, + JwtModule.registerAsync({ + useFactory: createJwtOptions, + }), + AccountModule, + SystemModule, + OauthModule, + RoleModule, + IdentityManagementModule, + CacheWrapperModule, + AuthGuardModule, + ], + providers: [ + UserRepo, + LegacySchoolRepo, + LocalStrategy, + AuthenticationService, + LdapService, + LdapStrategy, + Oauth2Strategy, + JwtWhitelistAdapter, + ], + exports: [AuthenticationService], +}) +export class AuthenticationTestModule {} diff --git a/apps/server/src/modules/authentication/authentication.module.ts b/apps/server/src/modules/authentication/authentication.module.ts index b608ed84c2d..8f1afbb1a17 100644 --- a/apps/server/src/modules/authentication/authentication.module.ts +++ b/apps/server/src/modules/authentication/authentication.module.ts @@ -1,4 +1,4 @@ -import { authConfig, AuthGuardModule } from '@infra/auth-guard'; +import { AuthGuardModule } from '@infra/auth-guard'; import { CacheWrapperModule } from '@infra/cache'; import { IdentityManagementModule } from '@infra/identity-management'; import { AccountModule } from '@modules/account'; @@ -6,11 +6,13 @@ import { OauthModule } from '@modules/oauth/oauth.module'; import { RoleModule } from '@modules/role'; import { SystemModule } from '@modules/system'; import { Module } from '@nestjs/common'; -import { JwtModule, JwtModuleOptions } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { LegacySchoolRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { SignOptions } from 'jsonwebtoken'; +import { Algorithm, SignOptions } from 'jsonwebtoken'; +import { AuthenticationConfig } from './authentication-config'; import { JwtWhitelistAdapter } from './helper/jwt-whitelist.adapter'; import { AuthenticationService } from './services/authentication.service'; import { LdapService } from './services/ldap.service'; @@ -18,25 +20,38 @@ import { LdapStrategy } from './strategy/ldap.strategy'; import { LocalStrategy } from './strategy/local.strategy'; import { Oauth2Strategy } from './strategy/oauth2.strategy'; -const { algorithm, audience, expiresIn, issuer, header } = authConfig.jwtOptions; -const signOptions: SignOptions = { - algorithm, - audience, - expiresIn, - issuer, - header: { ...header, alg: algorithm }, -}; -const jwtModuleOptions: JwtModuleOptions = { - secret: authConfig.secret, - signOptions, - verifyOptions: signOptions, +const createJwtOptions = (configService: ConfigService) => { + const algorithm = configService.getOrThrow('JWT_SIGNING_ALGORITHM'); + + const signOptions: SignOptions = { + algorithm, + expiresIn: configService.getOrThrow('JWT_LIFETIME'), + issuer: configService.getOrThrow('SC_DOMAIN'), + audience: configService.getOrThrow('SC_DOMAIN'), + header: { typ: 'JWT', alg: algorithm }, + }; + + const privateKey = configService.getOrThrow('JWT_PRIVATE_KEY'); + const publicKey = configService.getOrThrow('JWT_PUBLIC_KEY'); + + const options = { + privateKey, + publicKey, + signOptions, + verifyOptions: signOptions, + }; + + return options; }; @Module({ imports: [ LoggerModule, PassportModule, - JwtModule.register(jwtModuleOptions), + JwtModule.registerAsync({ + useFactory: createJwtOptions, + inject: [ConfigService], + }), AccountModule, SystemModule, OauthModule, diff --git a/apps/server/src/modules/authentication/services/authentication.service.spec.ts b/apps/server/src/modules/authentication/services/authentication.service.spec.ts index 31db4ac0e28..786e71420d2 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.spec.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.spec.ts @@ -185,14 +185,13 @@ describe('AuthenticationService', () => { }; it('should pass the correct parameters', async () => { - const { mockCurrentUser, expectedPayload, expiresIn } = setup(); + const { mockCurrentUser, expectedPayload } = setup(); await authenticationService.generateCurrentUserJwt(mockCurrentUser); expect(jwtService.sign).toBeCalledWith( expectedPayload, expect.objectContaining({ subject: mockCurrentUser.accountId, jwtid: expect.any(String), - expiresIn, }) ); }); diff --git a/apps/server/src/modules/authentication/services/authentication.service.ts b/apps/server/src/modules/authentication/services/authentication.service.ts index dd1864c575c..69336942b7a 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.ts @@ -2,7 +2,7 @@ import { CreateJwtPayload, ICurrentUser, JwtPayloadFactory } from '@infra/auth-g import { Account, AccountService } from '@modules/account'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { JwtService } from '@nestjs/jwt'; +import { JwtService, JwtSignOptions } from '@nestjs/jwt'; import { User } from '@shared/domain/entity'; import { Logger } from '@src/core/logger'; import { randomUUID } from 'crypto'; @@ -46,13 +46,22 @@ export class AuthenticationService { return account; } - private async generateJwt(createJwtPayload: CreateJwtPayload, expiresIn?: number | string): Promise { + private async generateJwtAndAddToWhitelist( + createJwtPayload: CreateJwtPayload, + expiresIn?: number | string + ): Promise { const jti = randomUUID(); - const accessToken = this.jwtService.sign(createJwtPayload, { + const options: JwtSignOptions = { subject: createJwtPayload.accountId, jwtid: jti, - expiresIn, - }); + }; + + // It is necessary to set expiresIn conditionally like this, because setting it to undefined in the JwtSignOptions overwrites the value from the JwtModuleOptions. + if (expiresIn) { + options.expiresIn = expiresIn; + } + + const accessToken = this.jwtService.sign(createJwtPayload, options); await this.jwtWhitelistAdapter.addToWhitelist(createJwtPayload.accountId, jti); @@ -61,8 +70,7 @@ export class AuthenticationService { public async generateCurrentUserJwt(currentUser: ICurrentUser): Promise { const createJwtPayload = JwtPayloadFactory.buildFromCurrentUser(currentUser); - const expiresIn = this.configService.get('JWT_LIFETIME'); - const jwtToken = await this.generateJwt(createJwtPayload, expiresIn); + const jwtToken = await this.generateJwtAndAddToWhitelist(createJwtPayload); return jwtToken; } @@ -78,7 +86,7 @@ export class AuthenticationService { const createJwtPayload = JwtPayloadFactory.buildFromSupportUser(currentUser, supportUser.id); const expiresIn = this.configService.get('JWT_LIFETIME_SUPPORT_SECONDS'); - const jwtToken = await this.generateJwt(createJwtPayload, expiresIn); + const jwtToken = await this.generateJwtAndAddToWhitelist(createJwtPayload, expiresIn); this.logger.info(new ShdUserCreateTokenLoggable(supportUser.id, targetUser.id, expiresIn)); diff --git a/apps/server/src/modules/board/board-collaboration.config.ts b/apps/server/src/modules/board/board-collaboration.config.ts index b1fc78cfdc1..18316af5729 100644 --- a/apps/server/src/modules/board/board-collaboration.config.ts +++ b/apps/server/src/modules/board/board-collaboration.config.ts @@ -6,6 +6,13 @@ export interface BoardCollaborationConfig { const boardCollaborationConfig = { NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, + + // Node's process.env escapes newlines. We need to reverse it for the keys to work. + // See: https://stackoverflow.com/questions/30400341/environment-variables-containing-newlines-in-node + JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'), + JWT_SIGNING_ALGORITHM: Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm, + SC_DOMAIN: Configuration.get('SC_DOMAIN') as string, + ADMIN_API__ALLOWED_API_KEYS: (Configuration.get('ADMIN_API__ALLOWED_API_KEYS') as string).split(','), }; export const config = () => boardCollaborationConfig; diff --git a/apps/server/src/modules/files-storage/files-storage.config.ts b/apps/server/src/modules/files-storage/files-storage.config.ts index a5e90d90b3e..b1a06834bc3 100644 --- a/apps/server/src/modules/files-storage/files-storage.config.ts +++ b/apps/server/src/modules/files-storage/files-storage.config.ts @@ -2,9 +2,11 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { AuthorizationClientConfig } from '@infra/authorization-client'; import { S3Config } from '@infra/s3-client'; import { CoreModuleConfig } from '@src/core'; +import { AuthGuardConfig } from '@src/infra/auth-guard'; +import { Algorithm } from 'jsonwebtoken'; export const FILES_STORAGE_S3_CONNECTION = 'FILES_STORAGE_S3_CONNECTION'; -export interface FileStorageConfig extends CoreModuleConfig, AuthorizationClientConfig { +export interface FileStorageConfig extends CoreModuleConfig, AuthorizationClientConfig, AuthGuardConfig { MAX_FILE_SIZE: number; MAX_SECURITY_CHECK_FILE_SIZE: number; USE_STREAM_TO_ANTIVIRUS: boolean; @@ -19,12 +21,23 @@ export const authorizationClientConfig: AuthorizationClientConfig = { basePath: `${Configuration.get('API_HOST') as string}/v3/`, }; +const authGuardConfig: AuthGuardConfig = { + ADMIN_API__ALLOWED_API_KEYS: (Configuration.get('ADMIN_API__ALLOWED_API_KEYS') as string).split(','), + + // Node's process.env escapes newlines. We need to reverse it for the keys to work. + // See: https://stackoverflow.com/questions/30400341/environment-variables-containing-newlines-in-node + JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'), + JWT_SIGNING_ALGORITHM: Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm, + SC_DOMAIN: Configuration.get('SC_DOMAIN') as string, +}; + const fileStorageConfig: FileStorageConfig = { MAX_FILE_SIZE: Configuration.get('FILES_STORAGE__MAX_FILE_SIZE') as number, MAX_SECURITY_CHECK_FILE_SIZE: Configuration.get('FILES_STORAGE__MAX_FILE_SIZE') as number, USE_STREAM_TO_ANTIVIRUS: Configuration.get('FILES_STORAGE__USE_STREAM_TO_ANTIVIRUS') as boolean, ...authorizationClientConfig, ...defaultConfig, + ...authGuardConfig, EXIT_ON_ERROR: Configuration.get('EXIT_ON_ERROR') as boolean, }; diff --git a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts index 6cfcb03b74f..8c2b6a55f72 100644 --- a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts +++ b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts @@ -15,6 +15,13 @@ export const s3Config: S3Config = { const fwuLearningContentsConfig = { NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, INCOMING_REQUEST_TIMEOUT: Configuration.get('FWU_CONTENT__INCOMING_REQUEST_TIMEOUT_MS') as number, + + // Node's process.env escapes newlines. We need to reverse it for the keys to work. + // See: https://stackoverflow.com/questions/30400341/environment-variables-containing-newlines-in-node + JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'), + JWT_SIGNING_ALGORITHM: Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm, + SC_DOMAIN: Configuration.get('SC_DOMAIN') as string, + ADMIN_API__ALLOWED_API_KEYS: (Configuration.get('ADMIN_API__ALLOWED_API_KEYS') as string).split(','), }; export const config = () => fwuLearningContentsConfig; diff --git a/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts b/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts index 0248552c576..d64eeba53eb 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts @@ -2,7 +2,7 @@ import { AuthorizationClientModule } from '@infra/authorization-client'; import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@infra/database'; import { RabbitMQWrapperTestModule } from '@infra/rabbitmq'; import { S3ClientModule } from '@infra/s3-client'; -import { AuthenticationApiModule } from '@modules/authentication/authentication-api.module'; +import { AuthenticationApiTestModule } from '@modules/authentication/authentication-api-test.module'; import { UserModule } from '@modules/user'; import { DynamicModule, Module } from '@nestjs/common'; import { ALL_ENTITIES } from '@shared/domain/entity'; @@ -20,7 +20,7 @@ import { H5PEditorUc } from './uc/h5p.uc'; const imports = [ H5PEditorModule, MongoMemoryDatabaseModule.forRoot({ entities: [...ALL_ENTITIES, H5PContent] }), - AuthenticationApiModule, + AuthenticationApiTestModule, AuthorizationClientModule.register(authorizationClientConfig), UserModule, CoreModule, diff --git a/apps/server/src/modules/h5p-editor/h5p-editor.config.ts b/apps/server/src/modules/h5p-editor/h5p-editor.config.ts index 19c9bf7467f..84509d1dd22 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor.config.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor.config.ts @@ -4,6 +4,7 @@ import { AuthorizationClientConfig } from '@infra/authorization-client'; import { S3Config } from '@infra/s3-client'; import { LanguageType } from '@shared/domain/interface'; import { CoreModuleConfig } from '@src/core'; +import { Algorithm } from 'jsonwebtoken'; export interface H5PEditorConfig extends CoreModuleConfig, AuthorizationClientConfig, AuthGuardConfig { NEST_LOG_LEVEL: string; @@ -20,9 +21,12 @@ const h5pEditorConfig: H5PEditorConfig = { ...authorizationClientConfig, EXIT_ON_ERROR: Configuration.get('EXIT_ON_ERROR') as boolean, ADMIN_API__ALLOWED_API_KEYS: [], - JWT_AUD: Configuration.get('JWT_AUD') as string, - JWT_LIFETIME: Configuration.get('JWT_LIFETIME') as string, - AUTHENTICATION: Configuration.get('AUTHENTICATION') as string, + + // Node's process.env escapes newlines. We need to reverse it for the keys to work. + // See: https://stackoverflow.com/questions/30400341/environment-variables-containing-newlines-in-node + JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'), + JWT_SIGNING_ALGORITHM: Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm, + SC_DOMAIN: Configuration.get('SC_DOMAIN') as string, }; export const translatorConfig = { diff --git a/apps/server/src/modules/server/admin-api-server.config.ts b/apps/server/src/modules/server/admin-api-server.config.ts index 93b855541c9..38b7ee16bd3 100644 --- a/apps/server/src/modules/server/admin-api-server.config.ts +++ b/apps/server/src/modules/server/admin-api-server.config.ts @@ -1,11 +1,12 @@ -import { DeletionConfig } from '@modules/deletion'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { AuthGuardConfig } from '@infra/auth-guard'; +import { DeletionConfig } from '@modules/deletion'; import { LegacySchoolConfig } from '@modules/legacy-school'; -import { UserConfig } from '@modules/user'; import { RegistrationPinConfig } from '@modules/registration-pin'; import { ToolConfig } from '@modules/tool'; -import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { UserConfig } from '@modules/user'; import { LanguageType } from '@shared/domain/interface'; +import { Algorithm } from 'jsonwebtoken'; export interface AdminApiServerConfig extends DeletionConfig, @@ -56,9 +57,11 @@ const config: AdminApiServerConfig = { ADMIN_API__ALLOWED_API_KEYS: (Configuration.get('ADMIN_API__ALLOWED_API_KEYS') as string) .split(',') .map((part) => (part.split(':').pop() ?? '').trim()), - JWT_AUD: Configuration.get('JWT_AUD') as string, - JWT_LIFETIME: Configuration.get('JWT_LIFETIME') as string, - AUTHENTICATION: Configuration.get('AUTHENTICATION') as string, + + // Node's process.env escapes newlines. We need to reverse it for the keys to work. + // See: https://stackoverflow.com/questions/30400341/environment-variables-containing-newlines-in-node + JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'), + JWT_SIGNING_ALGORITHM: Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm, LOGIN_BLOCK_TIME: 0, TEACHER_STUDENT_VISIBILITY__IS_CONFIGURABLE: Configuration.get( 'TEACHER_STUDENT_VISIBILITY__IS_CONFIGURABLE' @@ -70,6 +73,7 @@ const config: AdminApiServerConfig = { TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION: Configuration.get( 'TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION' ) as string, + SC_DOMAIN: Configuration.get('SC_DOMAIN') as string, }; export const adminApiServerConfig = () => config; diff --git a/apps/server/src/modules/server/admin-api.server.module.ts b/apps/server/src/modules/server/admin-api.server.module.ts index 8f380ff96bb..342170b7873 100644 --- a/apps/server/src/modules/server/admin-api.server.module.ts +++ b/apps/server/src/modules/server/admin-api.server.module.ts @@ -1,12 +1,13 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { AuthGuardModule } from '@infra/auth-guard'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; +import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { DeletionApiModule } from '@modules/deletion/deletion-api.module'; import { FileEntity } from '@modules/files/entity'; import { LegacySchoolAdminApiModule } from '@modules/legacy-school/legacy-school-admin.api-module'; import { ToolAdminApiModule } from '@modules/tool/tool-admin-api.module'; import { UserAdminApiModule } from '@modules/user/user-admin-api.module'; -import { DynamicModule, Module } from '@nestjs/common'; +import { DynamicModule, Module, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { CqrsModule } from '@nestjs/cqrs'; import { ALL_ENTITIES } from '@shared/domain/entity'; @@ -16,7 +17,6 @@ import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@src/infr import { EtherpadClientModule } from '@src/infra/etherpad-client'; import { RabbitMQWrapperModule, RabbitMQWrapperTestModule } from '@src/infra/rabbitmq'; import { AdminApiRegistrationPinModule } from '../registration-pin/admin-api-registration-pin.module'; -import { defaultMikroOrmOptions } from './server.module'; import { adminApiServerConfig } from './admin-api-server.config'; const serverModules = [ @@ -33,6 +33,12 @@ const serverModules = [ AuthGuardModule, ]; +const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { + findOneOrFailHandler: (entityName: string, where: Dictionary | IPrimaryKey) => + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + new NotFoundException(`The requested ${entityName}: ${where} has not been found.`), +}; + @Module({ imports: [ RabbitMQWrapperModule, diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 610bda8f962..caeb32250f8 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -32,6 +32,7 @@ import type { BbbConfig } from '@modules/video-conference/bbb'; import type { LanguageType } from '@shared/domain/interface'; import { SchulcloudTheme } from '@shared/domain/types'; import type { CoreModuleConfig } from '@src/core'; +import { Algorithm } from 'jsonwebtoken'; import type { Timezone } from './types/timezone.enum'; export enum NodeEnvType { @@ -131,7 +132,6 @@ export interface ServerConfig } const config: ServerConfig = { - AUTHENTICATION: Configuration.get('AUTHENTICATION') as string, ACCESSIBILITY_REPORT_EMAIL: Configuration.get('ACCESSIBILITY_REPORT_EMAIL') as string, ADMIN_TABLES_DISPLAY_CONSENT_COLUMN: Configuration.get('ADMIN_TABLES_DISPLAY_CONSENT_COLUMN') as boolean, ALERT_STATUS_URL: @@ -183,6 +183,12 @@ const config: ServerConfig = { JWT_TIMEOUT_SECONDS: Configuration.get('JWT_TIMEOUT_SECONDS') as number, JWT_LIFETIME_SUPPORT_SECONDS: Configuration.get('JWT_LIFETIME_SUPPORT_SECONDS') as number, JWT_EXTENDED_TIMEOUT_SECONDS: Configuration.get('JWT_EXTENDED_TIMEOUT_SECONDS') as number, + + // Node's process.env escapes newlines. We need to reverse it for the keys to work. + // See: https://stackoverflow.com/questions/30400341/environment-variables-containing-newlines-in-node + JWT_PRIVATE_KEY: (Configuration.get('JWT_PRIVATE_KEY') as string).replace(/\\n/g, '\n'), + JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'), + JWT_SIGNING_ALGORITHM: Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm, NOT_AUTHENTICATED_REDIRECT_URL: Configuration.get('NOT_AUTHENTICATED_REDIRECT_URL') as string, DOCUMENT_BASE_DIR: Configuration.get('DOCUMENT_BASE_DIR') as string, SC_THEME: Configuration.get('SC_THEME') as SchulcloudTheme, diff --git a/apps/server/src/modules/tldraw/config.ts b/apps/server/src/modules/tldraw/config.ts index bef12dde3c2..550cbd3609f 100644 --- a/apps/server/src/modules/tldraw/config.ts +++ b/apps/server/src/modules/tldraw/config.ts @@ -56,6 +56,13 @@ const tldrawConfig = { TLDRAW_MAX_DOCUMENT_SIZE: Configuration.get('TLDRAW__MAX_DOCUMENT_SIZE') as number, TLDRAW_FINALIZE_DELAY: Configuration.get('TLDRAW__FINALIZE_DELAY') as number, PERFORMANCE_MEASURE_ENABLED: Configuration.get('TLDRAW__PERFORMANCE_MEASURE_ENABLED') as boolean, + + // Node's process.env escapes newlines. We need to reverse it for the keys to work. + // See: https://stackoverflow.com/questions/30400341/environment-variables-containing-newlines-in-node + JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'), + JWT_SIGNING_ALGORITHM: Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm, + SC_DOMAIN: Configuration.get('SC_DOMAIN') as string, + ADMIN_API__ALLOWED_API_KEYS: (Configuration.get('ADMIN_API__ALLOWED_API_KEYS') as string).split(','), }; export const config = () => tldrawConfig; diff --git a/config/default.schema.json b/config/default.schema.json index d836f48b4bf..515a9a58116 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -119,9 +119,18 @@ "default": 86400, "description": "Maximum time in seconds a generated pin is handled as valid." }, - "AUTHENTICATION": { + "JWT_PRIVATE_KEY": { "type": "string", - "description": "The secret that is used for create and validate the jwt." + "description": "The private key used to sign JWTs." + }, + "JWT_PUBLIC_KEY": { + "type": "string", + "description": "The public key used to verify JWTs." + }, + "JWT_SIGNING_ALGORITHM": { + "type": "string", + "default": "RS256", + "description": "The algorithm used to sign JWTs. Must be an asymmetric algorithm." }, "JWT_LIFETIME": { "type": "string", @@ -1125,11 +1134,6 @@ "default": false, "description": "Enabled that every request is logged with the information route and method" }, - "JWT_AUD": { - "type": "string", - "default": "https://dbildungscloud.de", - "description": "It is the organisation web point that is created the jwt and where questions can addressed." - }, "SUPPORT_PROBLEM_EMAIL_ADDRESS": { "type": "string", "default": "ticketsystem@dbildungscloud.de", diff --git a/config/development.json b/config/development.json index 14c1d4beac3..382b6df002f 100644 --- a/config/development.json +++ b/config/development.json @@ -1,6 +1,7 @@ { "$schema": "./default.schema.json", - "AUTHENTICATION": "secrets", + "JWT_PRIVATE_KEY": "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n", + "JWT_PUBLIC_KEY": "-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEA0/oW2sIZWvVt0AEgQ8PS80/udJzfWXu6t2QWjUcQA2THGvDSXXMH\n6YMMY2czyBgf6L7hHV/9p1Trfpe7YgxYhOoGsxhXG1keAYQ4+mdveaUAa3uiACdE\nodsB0OFjVUdgOHCyUIXFfhSsp2p2tmZeFi/bE2v/05kYO+ExgQuzUDbB8bCr1sc7\ngMS/2dC2iE/BVw/I0F14oZkZn0fshojg4qoaLbLVKB7Iw53IXF2878zXp81Jdnnv\nHdwVbGWqoII6sHZFQs8ob5S/WGMl4QnBHN98x0KmORUFyTv5kK4cdcC8LJ1HpoVW\nNC6js84iF9yFRhYXY2RHqh7BwaZZ4XZym/MetTdQTBDaSvhXe0A3WdahNG+DGrie\nhd6doWk98Adb49InaodH64ZRkurxiX61GEtzjMRq9EfGS5R/IfcWyPQbiir6ymKX\nfOUtywRjcm3FZzmT7j3c0UHzQVEH0NBfTMj+QKz5NILNP230j0DcjNImDbHHcVH1\nquSb6e0WXjKANTkf4gaTOw7jdQDFw0Ou3aEmwPg+Xk1cwCwSHOOmPSSssZwgjpzG\nodPO3vsMGfRYTwcGbzgdQFFj0qTmvgnM5MHtEy8qCyvM4OsAPnE0zQWn48p7PVdJ\nm6j0H/1BYgVw1KxecIVk/HryoTOkgS9lhLu8iEIyrpAlWascIK7Uw58CAwEAAQ==\n-----END RSA PUBLIC KEY-----\n", "SC_DOMAIN": "localhost", "PUBLIC_BACKEND_URL": "http://localhost:3030/api", "FEATURE_API_VALIDATION_ENABLED": true, diff --git a/config/test.json b/config/test.json index 70b8ff33e06..06ac1d84582 100644 --- a/config/test.json +++ b/config/test.json @@ -1,6 +1,7 @@ { "$schema": "./default.schema.json", - "AUTHENTICATION": "secrets", + "JWT_PRIVATE_KEY": "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n", + "JWT_PUBLIC_KEY": "-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEA0/oW2sIZWvVt0AEgQ8PS80/udJzfWXu6t2QWjUcQA2THGvDSXXMH\n6YMMY2czyBgf6L7hHV/9p1Trfpe7YgxYhOoGsxhXG1keAYQ4+mdveaUAa3uiACdE\nodsB0OFjVUdgOHCyUIXFfhSsp2p2tmZeFi/bE2v/05kYO+ExgQuzUDbB8bCr1sc7\ngMS/2dC2iE/BVw/I0F14oZkZn0fshojg4qoaLbLVKB7Iw53IXF2878zXp81Jdnnv\nHdwVbGWqoII6sHZFQs8ob5S/WGMl4QnBHN98x0KmORUFyTv5kK4cdcC8LJ1HpoVW\nNC6js84iF9yFRhYXY2RHqh7BwaZZ4XZym/MetTdQTBDaSvhXe0A3WdahNG+DGrie\nhd6doWk98Adb49InaodH64ZRkurxiX61GEtzjMRq9EfGS5R/IfcWyPQbiir6ymKX\nfOUtywRjcm3FZzmT7j3c0UHzQVEH0NBfTMj+QKz5NILNP230j0DcjNImDbHHcVH1\nquSb6e0WXjKANTkf4gaTOw7jdQDFw0Ou3aEmwPg+Xk1cwCwSHOOmPSSssZwgjpzG\nodPO3vsMGfRYTwcGbzgdQFFj0qTmvgnM5MHtEy8qCyvM4OsAPnE0zQWn48p7PVdJ\nm6j0H/1BYgVw1KxecIVk/HryoTOkgS9lhLu8iEIyrpAlWascIK7Uw58CAwEAAQ==\n-----END RSA PUBLIC KEY-----\n", "host": "localhost", "port": 3030, "SC_DOMAIN": "localhost", diff --git a/src/services/authentication/configuration.js b/src/services/authentication/configuration.js index 3ea49809981..d40335db846 100644 --- a/src/services/authentication/configuration.js +++ b/src/services/authentication/configuration.js @@ -1,19 +1,24 @@ const { Configuration } = require('@hpi-schul-cloud/commons'); module.exports = { - audience: Configuration.get('JWT_AUD'), + audience: Configuration.get('SC_DOMAIN'), authConfig: { entity: 'account', // name of the found user in the request context // entityId and service are never queried, but need to be provided otherwise the server doesn't start entityId: 'id', service: 'emptyService', // This service is registered in 'index.js' - secret: Configuration.get('AUTHENTICATION'), + + // The idea to concatenate the keys is from this feathers issue: https://github.com/feathersjs/feathers/issues/1251 + // Furthermore: Node's process.env escapes newlines. We need to reverse it for the keys to work. + secret: + Configuration.get('JWT_PRIVATE_KEY').replace(/\\n/g, '\n') + + Configuration.get('JWT_PUBLIC_KEY').replace(/\\n/g, '\n'), authStrategies: ['jwt', 'tsp', 'api-key'], jwtOptions: { - header: { typ: 'access' }, - audience: Configuration.get('JWT_AUD'), - issuer: 'feathers', - algorithm: 'HS256', + header: { typ: 'JWT' }, + audience: Configuration.get('SC_DOMAIN'), + issuer: Configuration.get('SC_DOMAIN'), + algorithm: Configuration.get('JWT_SIGNING_ALGORITHM'), expiresIn: Configuration.get('JWT_LIFETIME'), }, tsp: {},