From 667aab1f3651b91ab73a8d9c66f3af6f92b7b271 Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Sat, 26 Apr 2025 17:39:20 +0200 Subject: [PATCH] feat(fullstack): Add JWT validation --- .../api/legalconsenthub.yml | 11 +++++- legalconsenthub-backend/build.gradle | 5 ++- .../legalconsenthub/config/SecurityConfig.kt | 39 +++++++++++++++++++ .../src/main/resources/application.yaml | 15 +++++++ .../applicationForm/useApplicationFormApi.ts | 2 +- .../useApplicationFormTemplateApi.ts | 5 ++- legalconsenthub/server/utils/auth.ts | 12 +++++- 7 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt diff --git a/legalconsenthub-backend/api/legalconsenthub.yml b/legalconsenthub-backend/api/legalconsenthub.yml index b9ecc96..b40493e 100644 --- a/legalconsenthub-backend/api/legalconsenthub.yml +++ b/legalconsenthub-backend/api/legalconsenthub.yml @@ -8,7 +8,10 @@ info: email: denis.lugowski@gmail.com servers: - - url: / + - url: http://localhost:8080 + +security: + - bearerAuth: [] paths: /application-forms: @@ -592,6 +595,12 @@ paths: $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: ####### UserDto ####### UserDto: diff --git a/legalconsenthub-backend/build.gradle b/legalconsenthub-backend/build.gradle index 5e495ec..9f9d09c 100644 --- a/legalconsenthub-backend/build.gradle +++ b/legalconsenthub-backend/build.gradle @@ -24,9 +24,12 @@ dependencies { implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' implementation 'org.jetbrains.kotlin:kotlin-reflect' implementation 'org.liquibase:liquibase-core' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6' implementation 'org.springdoc:springdoc-openapi-ui:1.8.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + // https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html#oauth2-resource-server-access-token-jwt + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'com.h2database:h2' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt new file mode 100644 index 0000000..5d51376 --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt @@ -0,0 +1,39 @@ +package com.betriebsratkanzlei.legalconsenthub.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm +import org.springframework.security.oauth2.jwt.JwtDecoder +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder +import org.springframework.security.web.SecurityFilterChain + +@Configuration +@EnableWebSecurity +class SecurityConfig { + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + csrf { disable() } + authorizeHttpRequests { + authorize("/swagger-ui/**", permitAll) + authorize("/v3/**", permitAll) + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + jwt { } + } + } + + return http.build() + } + + @Bean + fun jwtDecoder(): JwtDecoder { + return NimbusJwtDecoder.withJwkSetUri("http://192.168.178.105:3001/api/auth/jwks") + .jwsAlgorithm(SignatureAlgorithm.ES512).build() + } +} diff --git a/legalconsenthub-backend/src/main/resources/application.yaml b/legalconsenthub-backend/src/main/resources/application.yaml index 59a2b19..e3b5fc0 100644 --- a/legalconsenthub-backend/src/main/resources/application.yaml +++ b/legalconsenthub-backend/src/main/resources/application.yaml @@ -22,6 +22,14 @@ spring: order_inserts: true enable_lazy_load_no_trans: true +# security: +# oauth2: +# resourceserver: +# jwt: +# issuer-uri: http://192.168.178.105:3001 +# jwk-set-uri: http://192.168.178.105:3001/api/auth/jwks +# jws-algorithms: ES512 + liquibase: enabled: true drop-first: false @@ -31,3 +39,10 @@ spring: sql: init: platform: h2 + +logging: + level: + org: + springframework: + security: TRACE + oauth2: TRACE diff --git a/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts b/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts index cafc7a0..fc8f6c1 100644 --- a/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts +++ b/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts @@ -12,7 +12,7 @@ export function useApplicationFormApi() { ) const applicationFormApiClient = new ApplicationFormApi( - new Configuration({ basePath, headers: { Authorization: jwt.value ?? '' } }) + new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } }) ) async function createApplicationForm( diff --git a/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplateApi.ts b/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplateApi.ts index a693397..38b748d 100644 --- a/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplateApi.ts +++ b/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplateApi.ts @@ -5,12 +5,15 @@ import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo' export function useApplicationFormTemplateApi() { const appBaseUrl = useRuntimeConfig().app.baseURL const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public + const { jwt } = useAuth() const basePath = withoutTrailingSlash( cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath) ) - const applicationFormApiClient = new ApplicationFormTemplateApi(new Configuration({ basePath })) + const applicationFormApiClient = new ApplicationFormTemplateApi( + new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } }) + ) async function createApplicationFormTemplate( createApplicationFormDto: CreateApplicationFormDto diff --git a/legalconsenthub/server/utils/auth.ts b/legalconsenthub/server/utils/auth.ts index 8487e3a..4035891 100644 --- a/legalconsenthub/server/utils/auth.ts +++ b/legalconsenthub/server/utils/auth.ts @@ -7,7 +7,17 @@ export const auth = betterAuth({ database: new Database('./sqlite.db'), emailAndPassword: { enabled: true, autoSignIn: false }, plugins: [ - jwt(), + jwt({ + jwt: { + issuer: 'http://192.168.178.105:3001' + }, + jwks: { + keyPairConfig: { + // Supported by NimbusJwtDecoder + alg: 'ES512' + } + } + }), organization({ async sendInvitationEmail(data) { console.log('Sending invitation email', data)