From bb4a6dc9076c23a69ba4722fb87aca8d4e05da20 Mon Sep 17 00:00:00 2001
From: kashiuno <taigusiobaka@gmail.com>
Date: Wed, 8 Jan 2025 08:13:11 +0300
Subject: [PATCH] Add jdbc storage for clients

---
 build.gradle.kts                              |  2 +
 .../KotlinRegisteredClientRowMapper.kt        | 66 +++++++++++++++++++
 .../qr_access_auth_server/SecurityConfig.kt   | 38 +++++------
 src/main/resources/application.yaml           |  7 ++
 4 files changed, 91 insertions(+), 22 deletions(-)
 create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_auth_server/KotlinRegisteredClientRowMapper.kt

diff --git a/build.gradle.kts b/build.gradle.kts
index 8be4085..5c89058 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -20,6 +20,8 @@ repositories {
 
 dependencies {
 	implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
+	implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
+	runtimeOnly("org.postgresql:postgresql")
 	implementation("org.jetbrains.kotlin:kotlin-reflect")
 	testImplementation("org.springframework.boot:spring-boot-starter-test")
 	testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
diff --git a/src/main/kotlin/ru/vyatsu/qr_access_auth_server/KotlinRegisteredClientRowMapper.kt b/src/main/kotlin/ru/vyatsu/qr_access_auth_server/KotlinRegisteredClientRowMapper.kt
new file mode 100644
index 0000000..a486cb3
--- /dev/null
+++ b/src/main/kotlin/ru/vyatsu/qr_access_auth_server/KotlinRegisteredClientRowMapper.kt
@@ -0,0 +1,66 @@
+package ru.vyatsu.qr_access_auth_server
+
+import org.springframework.security.oauth2.core.AuthorizationGrantType
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod
+import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientRowMapper
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient
+import org.springframework.util.StringUtils
+import java.sql.ResultSet
+
+class KotlinRegisteredClientRowMapper : RegisteredClientRowMapper() {
+
+    override fun mapRow(rs: ResultSet, rowNum: Int): RegisteredClient? {
+        val clientIdIssuedAt = rs.getTimestamp("client_id_issued_at")
+        val clientSecretExpiresAt = rs.getTimestamp("client_secret_expires_at")
+        val clientAuthenticationMethods =
+            StringUtils.commaDelimitedListToSet(rs.getString("client_authentication_methods"))
+        val authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorization_grant_types"))
+        val redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirect_uris"))
+        val postLogoutRedirectUris = StringUtils.commaDelimitedListToSet(rs.getString("post_logout_redirect_uris"))
+        val clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"))
+        val builder = RegisteredClient.withId(rs.getString("id"))
+            .clientId(rs.getString("client_id"))
+            .clientIdIssuedAt(clientIdIssuedAt?.toInstant())
+            .clientSecret(rs.getString("client_secret"))
+            .clientSecretExpiresAt(clientSecretExpiresAt?.toInstant())
+            .clientName(rs.getString("client_name"))
+            .clientAuthenticationMethods { authenticationMethods ->
+                clientAuthenticationMethods.forEach { authenticationMethod ->
+                    authenticationMethods.add(resolveClientAuthenticationMethod(authenticationMethod))
+                }
+            }
+            .authorizationGrantTypes { grantTypes ->
+                authorizationGrantTypes.forEach { grantType ->
+                    grantTypes.add(resolveAuthorizationGrantType(grantType))
+                }
+            }
+            .redirectUris { uris -> uris.addAll(redirectUris) }
+            .postLogoutRedirectUris { uris ->
+                uris.addAll(postLogoutRedirectUris)
+            }
+            .scopes { scopes -> scopes.addAll(clientScopes) }
+        return builder.build()
+    }
+
+    private fun resolveAuthorizationGrantType(authorizationGrantType: String): AuthorizationGrantType {
+        return if (AuthorizationGrantType.AUTHORIZATION_CODE.value == authorizationGrantType) {
+            AuthorizationGrantType.AUTHORIZATION_CODE
+        } else if (AuthorizationGrantType.CLIENT_CREDENTIALS.value == authorizationGrantType) {
+            AuthorizationGrantType.CLIENT_CREDENTIALS
+        } else {
+            if (AuthorizationGrantType.REFRESH_TOKEN.value == authorizationGrantType) AuthorizationGrantType.REFRESH_TOKEN
+            else AuthorizationGrantType(authorizationGrantType)
+        }
+    }
+
+    private fun resolveClientAuthenticationMethod(clientAuthenticationMethod: String): ClientAuthenticationMethod {
+        return if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.value == clientAuthenticationMethod) {
+            ClientAuthenticationMethod.CLIENT_SECRET_BASIC
+        } else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.value == clientAuthenticationMethod) {
+            ClientAuthenticationMethod.CLIENT_SECRET_POST
+        } else {
+            if (ClientAuthenticationMethod.NONE.value == clientAuthenticationMethod) ClientAuthenticationMethod.NONE
+            else ClientAuthenticationMethod(clientAuthenticationMethod)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/ru/vyatsu/qr_access_auth_server/SecurityConfig.kt b/src/main/kotlin/ru/vyatsu/qr_access_auth_server/SecurityConfig.kt
index 234eb67..dec916f 100644
--- a/src/main/kotlin/ru/vyatsu/qr_access_auth_server/SecurityConfig.kt
+++ b/src/main/kotlin/ru/vyatsu/qr_access_auth_server/SecurityConfig.kt
@@ -1,5 +1,6 @@
 package ru.vyatsu.qr_access_auth_server
 
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import com.nimbusds.jose.jwk.JWKSet
 import com.nimbusds.jose.jwk.RSAKey
 import com.nimbusds.jose.jwk.source.ImmutableJWKSet
@@ -8,18 +9,17 @@ import com.nimbusds.jose.proc.SecurityContext
 import org.springframework.context.annotation.Bean
 import org.springframework.context.annotation.Configuration
 import org.springframework.core.annotation.Order
+import org.springframework.jdbc.core.JdbcTemplate
 import org.springframework.security.config.annotation.web.builders.HttpSecurity
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
-import org.springframework.security.core.userdetails.User
 import org.springframework.security.core.userdetails.UserDetailsService
-import org.springframework.security.oauth2.core.AuthorizationGrantType
-import org.springframework.security.oauth2.core.ClientAuthenticationMethod
+import org.springframework.security.jackson2.SecurityJackson2Modules
 import org.springframework.security.oauth2.jwt.JwtDecoder
-import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository
-import org.springframework.security.oauth2.server.authorization.client.RegisteredClient
+import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
 import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
 import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer
+import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
 import org.springframework.security.provisioning.InMemoryUserDetailsManager
 import org.springframework.security.web.SecurityFilterChain
@@ -51,31 +51,25 @@ class SecurityConfig {
     fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
         http.csrf { it.disable() }
             .authorizeHttpRequests { it.anyRequest().authenticated() }
-
         return http.build()
     }
 
     @Bean
     fun userDetailsService(): UserDetailsService {
-        return InMemoryUserDetailsManager(
-            User.withDefaultPasswordEncoder()
-                .username("user")
-                .password("password")
-                .roles("USER")
-                .build()
-        )
+        return InMemoryUserDetailsManager()
     }
 
     @Bean
-    fun registeredClientRepository(): RegisteredClientRepository {
-        val testClient = RegisteredClient.withId(UUID.randomUUID().toString())
-            .clientId("test-client")
-            .clientSecret("{noop}secret")
-            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
-            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
-            .build()
-
-        return InMemoryRegisteredClientRepository(testClient)
+    fun registeredClientRepository(operations: JdbcTemplate): RegisteredClientRepository {
+        val clientRepository = JdbcRegisteredClientRepository(operations)
+        val clientRowMapper = KotlinRegisteredClientRowMapper()
+        val classLoader = JdbcRegisteredClientRepository::class.java.classLoader
+        val objectMapper = jacksonObjectMapper()
+        objectMapper.registerModules(SecurityJackson2Modules.getModules(classLoader))
+        objectMapper.registerModule(OAuth2AuthorizationServerJackson2Module())
+        clientRowMapper.setObjectMapper(objectMapper)
+        clientRepository.setRegisteredClientRowMapper(clientRowMapper)
+        return clientRepository
     }
 
     @Bean
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index e69de29..ed7e5e5 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -0,0 +1,7 @@
+spring:
+  application:
+    name: qr-access-auth-server
+  datasource:
+    url: jdbc:postgresql://localhost:5432/qr_access
+    username: qr_access_user
+    password: 123
\ No newline at end of file