diff --git a/build.gradle.kts b/build.gradle.kts index 1f52b4a9..4d19ebab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -135,10 +135,15 @@ dependencies { ksp("io.insert-koin:koin-ksp-compiler:1.2.0") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") compileOnly("io.swagger.core.v3:swagger-annotations:2.2.6") implementation("io.swagger.core.v3:swagger-models:2.2.6") + implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version") + implementation("org.jetbrains.exposed:spring-transaction:$exposed_version") implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version") diff --git a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt new file mode 100644 index 00000000..a127762a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt @@ -0,0 +1,112 @@ +package dev.usbharu.hideout.config + +import com.nimbusds.jose.jwk.JWKSet +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.jwk.source.ImmutableJWKSet +import com.nimbusds.jose.jwk.source.JWKSource +import com.nimbusds.jose.proc.SecurityContext +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.http.MediaType +import org.springframework.security.config.Customizer +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.oauth2.jwt.JwtDecoder +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher +import java.security.KeyPairGenerator +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.util.* + +@EnableWebSecurity +@Configuration +class SecurityConfig { + + @Bean + @Order(1) + fun oauth2SecurityFilterChain(http: HttpSecurity): SecurityFilterChain { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) + http + .exceptionHandling { + it.defaultAuthenticationEntryPointFor( + LoginUrlAuthenticationEntryPoint("/login"), MediaTypeRequestMatcher(MediaType.TEXT_HTML) + ) + } + .oauth2ResourceServer { + it.jwt(Customizer.withDefaults()) + } + return http.build() + } + + + @Bean + @Order(2) + fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { + http + .authorizeHttpRequests { + it.anyRequest().authenticated() + } + .formLogin(Customizer.withDefaults()) + return http.build() + } + + @Bean + fun passwordEncoder(): PasswordEncoder { + return BCryptPasswordEncoder() + } + + @Bean + fun genJwkSource(): JWKSource { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(2048) + val generateKeyPair = keyPairGenerator.generateKeyPair() + val rsaPublicKey = generateKeyPair.public as RSAPublicKey + val rsaPrivateKey = generateKeyPair.private as RSAPrivateKey + val rsaKey = RSAKey + .Builder(rsaPublicKey) + .privateKey(rsaPrivateKey) + .keyID(UUID.randomUUID().toString()) + .build() + + val jwkSet = JWKSet(rsaKey) + return ImmutableJWKSet(jwkSet) + } + + @Bean + @ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "") + fun loadJwkSource(jwkConfig: JwkConfig): JWKSource { + val rsaKey = RSAKey.Builder(jwkConfig.publicKey) + .privateKey(jwkConfig.privateKey) + .keyID(jwkConfig.keyId) + .build() + return ImmutableJWKSet(JWKSet(rsaKey)) + } + + @Bean + fun jwtDecoder(jwkSource: JWKSource): JwtDecoder { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource) + } + + @Bean + fun authorizationServerSettings(): AuthorizationServerSettings { + return AuthorizationServerSettings.builder().build() + } +} + + +@ConfigurationProperties("hideout.security.jwt") +@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "") +data class JwkConfig( + val keyId: String, + val publicKey: RSAPublicKey, + val privateKey: RSAPrivateKey +) diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/DefaultApiImpl.kt b/src/main/kotlin/dev/usbharu/hideout/controller/DefaultApiImpl.kt new file mode 100644 index 00000000..32e21fb2 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/controller/DefaultApiImpl.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.controller + +import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken +import dev.usbharu.hideout.service.api.UserAuthApiService +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller + +@Controller +class DefaultApiImpl(private val userAuthApiService: UserAuthApiService) : DefaultApi { + override fun refreshTokenPost(): ResponseEntity { + return ResponseEntity(HttpStatus.OK) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/RegisteredClientRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/RegisteredClientRepositoryImpl.kt new file mode 100644 index 00000000..cb43f90a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/RegisteredClientRepositoryImpl.kt @@ -0,0 +1,170 @@ +package dev.usbharu.hideout.repository + +import dev.usbharu.hideout.repository.RegisteredClient.clientId +import dev.usbharu.hideout.repository.RegisteredClient.clientSettings +import dev.usbharu.hideout.repository.RegisteredClient.tokenSettings +import dev.usbharu.hideout.util.JsonUtil +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.javatime.CurrentTimestamp +import org.jetbrains.exposed.sql.javatime.timestamp +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.security.oauth2.core.AuthorizationGrantType +import org.springframework.security.oauth2.core.ClientAuthenticationMethod +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings +import org.springframework.security.oauth2.server.authorization.settings.ConfigurationSettingNames +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings +import org.springframework.stereotype.Repository +import java.time.Instant +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient as SpringRegisteredClient + +@Repository +class RegisteredClientRepositoryImpl(private val database: Database) : RegisteredClientRepository { + + init { + transaction(database) { + SchemaUtils.create(RegisteredClient) + SchemaUtils.createMissingTablesAndColumns(RegisteredClient) + } + } + + override fun save(registeredClient: SpringRegisteredClient?) { + requireNotNull(registeredClient) + val singleOrNull = RegisteredClient.select { RegisteredClient.id eq registeredClient.id }.singleOrNull() + if (singleOrNull == null) { + RegisteredClient.insert { + it[id] = registeredClient.id + it[clientId] = registeredClient.clientId + it[clientIdIssuedAt] = registeredClient.clientIdIssuedAt ?: Instant.now() + it[clientSecret] = registeredClient.clientSecret + it[clientSecretExpiresAt] = registeredClient.clientSecretExpiresAt + it[clientName] = registeredClient.clientName + it[clientAuthenticationMethods] = registeredClient.clientAuthenticationMethods.joinToString(",") + it[authorizationGrantTypes] = registeredClient.authorizationGrantTypes.joinToString(",") + it[redirectUris] = registeredClient.redirectUris.joinToString(",") + it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",") + it[scopes] = registeredClient.scopes.joinToString(",") + it[clientSettings] = JsonUtil.mapToJson(registeredClient.clientSettings.settings) + it[tokenSettings] = JsonUtil.mapToJson(registeredClient.tokenSettings.settings) + } + } else { + RegisteredClient.update({ RegisteredClient.id eq registeredClient.id }) { + it[clientId] = registeredClient.clientId + it[clientIdIssuedAt] = registeredClient.clientIdIssuedAt ?: Instant.now() + it[clientSecret] = registeredClient.clientSecret + it[clientSecretExpiresAt] = registeredClient.clientSecretExpiresAt + it[clientName] = registeredClient.clientName + it[clientAuthenticationMethods] = registeredClient.clientAuthenticationMethods.joinToString(",") + it[authorizationGrantTypes] = registeredClient.authorizationGrantTypes.joinToString(",") + it[redirectUris] = registeredClient.redirectUris.joinToString(",") + it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",") + it[scopes] = registeredClient.scopes.joinToString(",") + it[clientSettings] = JsonUtil.mapToJson(registeredClient.clientSettings.settings) + it[tokenSettings] = JsonUtil.mapToJson(registeredClient.tokenSettings.settings) + } + } + } + + override fun findById(id: String?): SpringRegisteredClient? { + if (id == null) { + return null + } + return RegisteredClient.select { + RegisteredClient.id eq id + }.singleOrNull()?.toRegisteredClient() + } + + override fun findByClientId(clientId: String?): SpringRegisteredClient? { + if (clientId == null) { + return null + } + return RegisteredClient.select { + RegisteredClient.clientId eq clientId + }.singleOrNull()?.toRegisteredClient() + } +} + + +// org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql +object RegisteredClient : Table("registered_client") { + val id = varchar("id", 100) + val clientId = varchar("client_id", 100) + val clientIdIssuedAt = timestamp("client_id_issued_at").defaultExpression(CurrentTimestamp()) + val clientSecret = varchar("client_secret", 200).nullable().default(null) + val clientSecretExpiresAt = timestamp("client_secret_expires_at").nullable().default(null) + val clientName = varchar("client_name", 200) + val clientAuthenticationMethods = varchar("client_authentication_methods", 1000) + val authorizationGrantTypes = varchar("authorization_grant_types", 1000) + val redirectUris = varchar("redirect_uris", 1000).nullable().default(null) + val postLogoutRedirectUris = varchar("post_logout_redirect_uris", 1000).nullable().default(null) + val scopes = varchar("scopes", 1000) + val clientSettings = varchar("client_settings", 2000) + val tokenSettings = varchar("token_settings", 2000) + + override val primaryKey = PrimaryKey(id) +} + +fun ResultRow.toRegisteredClient(): SpringRegisteredClient { + + fun resolveClientAuthenticationMethods(string: String): ClientAuthenticationMethod { + return when (string) { + ClientAuthenticationMethod.CLIENT_SECRET_BASIC.value -> ClientAuthenticationMethod.CLIENT_SECRET_BASIC + ClientAuthenticationMethod.CLIENT_SECRET_JWT.value -> ClientAuthenticationMethod.CLIENT_SECRET_JWT + ClientAuthenticationMethod.CLIENT_SECRET_POST.value -> ClientAuthenticationMethod.CLIENT_SECRET_POST + ClientAuthenticationMethod.NONE.value -> ClientAuthenticationMethod.NONE + else -> { + ClientAuthenticationMethod(string) + } + } + } + + fun resolveAuthorizationGrantType(string: String): AuthorizationGrantType { + return when (string) { + AuthorizationGrantType.AUTHORIZATION_CODE.value -> AuthorizationGrantType.AUTHORIZATION_CODE + AuthorizationGrantType.CLIENT_CREDENTIALS.value -> AuthorizationGrantType.CLIENT_CREDENTIALS + AuthorizationGrantType.REFRESH_TOKEN.value -> AuthorizationGrantType.REFRESH_TOKEN + else -> { + AuthorizationGrantType(string) + } + } + } + + val clientAuthenticationMethods = this[RegisteredClient.clientAuthenticationMethods].split(",").toSet() + val authorizationGrantTypes = this[RegisteredClient.authorizationGrantTypes].split(",").toSet() + val redirectUris = this[RegisteredClient.redirectUris]?.split(",").orEmpty().toSet() + val postLogoutRedirectUris = this[RegisteredClient.postLogoutRedirectUris]?.split(",").orEmpty().toSet() + val clientScopes = this[RegisteredClient.scopes].split(",").toSet() + + val builder = SpringRegisteredClient + .withId(this[RegisteredClient.id]) + .clientId(this[clientId]) + .clientIdIssuedAt(this[RegisteredClient.clientIdIssuedAt]) + .clientSecret(this[RegisteredClient.clientSecret]) + .clientSecretExpiresAt(this[RegisteredClient.clientSecretExpiresAt]) + .clientName(this[RegisteredClient.clientName]) + .clientAuthenticationMethods { + clientAuthenticationMethods.forEach { s -> + it.add(resolveClientAuthenticationMethods(s)) + } + } + .authorizationGrantTypes { + authorizationGrantTypes.forEach { s -> + it.add(resolveAuthorizationGrantType(s)) + } + } + .redirectUris { it.addAll(redirectUris) } + .postLogoutRedirectUris { it.addAll(postLogoutRedirectUris) } + .scopes { it.addAll(clientScopes) } + .clientSettings(ClientSettings.withSettings(JsonUtil.jsonToMap(this[clientSettings])).build()) + + + val tokenSettingsMap = JsonUtil.jsonToMap(this[tokenSettings]) + val withSettings = TokenSettings.withSettings(tokenSettingsMap) + if (tokenSettingsMap.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) { + withSettings.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) + } + builder.tokenSettings(withSettings.build()) + + return builder.build() +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationConsentService.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationConsentService.kt new file mode 100644 index 00000000..45958141 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationConsentService.kt @@ -0,0 +1,65 @@ +package dev.usbharu.hideout.service.auth + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository +import org.springframework.stereotype.Service +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent as AuthorizationConsent + +@Service +class ExposedOAuth2AuthorizationConsentService(private val registeredClientRepository: RegisteredClientRepository) : + OAuth2AuthorizationConsentService { + override fun save(authorizationConsent: AuthorizationConsent?) { + requireNotNull(authorizationConsent) + val singleOrNull = + OAuth2AuthorizationConsent.select { OAuth2AuthorizationConsent.registeredClientId eq authorizationConsent.registeredClientId and (OAuth2AuthorizationConsent.principalName eq authorizationConsent.principalName) } + .singleOrNull() + if (singleOrNull == null) { + OAuth2AuthorizationConsent.insert { + it[registeredClientId] = authorizationConsent.registeredClientId + it[principalName] = authorizationConsent.principalName + it[authorities] = authorizationConsent.authorities.joinToString(",") + } + } + } + + override fun remove(authorizationConsent: AuthorizationConsent?) { + if (authorizationConsent == null) { + return + } + OAuth2AuthorizationConsent.deleteWhere { + registeredClientId eq authorizationConsent.registeredClientId and (principalName eq principalName) + } + } + + override fun findById(registeredClientId: String?, principalName: String?): AuthorizationConsent? { + requireNotNull(registeredClientId) + requireNotNull(principalName) + + return OAuth2AuthorizationConsent.select { OAuth2AuthorizationConsent.registeredClientId eq registeredClientId and (OAuth2AuthorizationConsent.principalName eq principalName) } + .singleOrNull()?.toAuthorizationConsent() + } + + fun ResultRow.toAuthorizationConsent(): AuthorizationConsent { + val registeredClientId = this[OAuth2AuthorizationConsent.registeredClientId] + val registeredClient = registeredClientRepository.findById(registeredClientId) + + val principalName = this[OAuth2AuthorizationConsent.principalName] + val builder = AuthorizationConsent.withId(registeredClientId, principalName) + + this[OAuth2AuthorizationConsent.authorities].split(",").forEach { + builder.authority(SimpleGrantedAuthority(it)) + } + + return builder.build() + } +} + +object OAuth2AuthorizationConsent : Table("oauth2_authorization_consent") { + val registeredClientId = varchar("registered_client_id", 100) + val principalName = varchar("principal_name", 200) + val authorities = varchar("authorities", 1000) + override val primaryKey = PrimaryKey(registeredClientId, principalName) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationService.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationService.kt new file mode 100644 index 00000000..8abea597 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationService.kt @@ -0,0 +1,329 @@ +package dev.usbharu.hideout.service.auth + +import dev.usbharu.hideout.util.JsonUtil +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.javatime.timestamp +import org.springframework.security.oauth2.core.* +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames +import org.springframework.security.oauth2.core.oidc.OidcIdToken +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository +import org.springframework.stereotype.Service + +@Service +class ExposedOAuth2AuthorizationService(private val registeredClientRepository: RegisteredClientRepository) : + OAuth2AuthorizationService { + override fun save(authorization: OAuth2Authorization?) { + requireNotNull(authorization) + val singleOrNull = Authorization.select { Authorization.id eq authorization.id }.singleOrNull() + if (singleOrNull == null) { + val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java) + val accessToken = authorization.getToken(OAuth2AccessToken::class.java) + val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java) + val oidcIdToken = authorization.getToken(OidcIdToken::class.java) + val userCode = authorization.getToken(OAuth2UserCode::class.java) + val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java) + Authorization.insert { + it[id] = authorization.id + it[registeredClientId] = authorization.registeredClientId + it[principalName] = authorization.principalName + it[authorizationGrantType] = authorization.authorizationGrantType.value + it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() } + it[attributes] = JsonUtil.mapToJson(authorization.attributes) + it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE) + it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue + it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt + it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt + it[authorizationCodeMetadata] = authorizationCodeToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[accessTokenValue] = accessToken?.token?.tokenValue + it[accessTokenIssuedAt] = accessToken?.token?.issuedAt + it[accessTokenExpiresAt] = accessToken?.token?.expiresAt + it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[accessTokenType] = accessToken?.token?.tokenType?.value + it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isEmpty() } + it[refreshTokenValue] = refreshToken?.token?.tokenValue + it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt + it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt + it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue + it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt + it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt + it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[userCodeValue] = userCode?.token?.tokenValue + it[userCodeIssuedAt] = userCode?.token?.issuedAt + it[userCodeExpiresAt] = userCode?.token?.expiresAt + it[userCodeMetadata] = userCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[deviceCodeValue] = deviceCode?.token?.tokenValue + it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt + it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt + it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + } + } else { + val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java) + val accessToken = authorization.getToken(OAuth2AccessToken::class.java) + val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java) + val oidcIdToken = authorization.getToken(OidcIdToken::class.java) + val userCode = authorization.getToken(OAuth2UserCode::class.java) + val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java) + Authorization.update({ Authorization.id eq authorization.id }) { + it[registeredClientId] = authorization.registeredClientId + it[principalName] = authorization.principalName + it[authorizationGrantType] = authorization.authorizationGrantType.value + it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() } + it[attributes] = JsonUtil.mapToJson(authorization.attributes) + it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE) + it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue + it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt + it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt + it[authorizationCodeMetadata] = authorizationCodeToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[accessTokenValue] = accessToken?.token?.tokenValue + it[accessTokenIssuedAt] = accessToken?.token?.issuedAt + it[accessTokenExpiresAt] = accessToken?.token?.expiresAt + it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[accessTokenType] = accessToken?.token?.tokenType?.value + it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isEmpty() } + it[refreshTokenValue] = refreshToken?.token?.tokenValue + it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt + it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt + it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue + it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt + it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt + it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[userCodeValue] = userCode?.token?.tokenValue + it[userCodeIssuedAt] = userCode?.token?.issuedAt + it[userCodeExpiresAt] = userCode?.token?.expiresAt + it[userCodeMetadata] = userCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + it[deviceCodeValue] = deviceCode?.token?.tokenValue + it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt + it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt + it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + } + } + + } + + override fun remove(authorization: OAuth2Authorization?) { + if (authorization == null) { + return + } + Authorization.deleteWhere { Authorization.id eq authorization.id } + } + + override fun findById(id: String?): OAuth2Authorization? { + if (id == null) { + return null + } + return Authorization.select { Authorization.id eq id }.singleOrNull()?.toAuthorization() + } + + override fun findByToken(token: String?, tokenType: OAuth2TokenType?): OAuth2Authorization? { + requireNotNull(token) + return when (tokenType?.value) { + null -> { + Authorization.select { + Authorization.authorizationCodeValue eq token + }.orWhere { + Authorization.accessTokenValue eq token + }.orWhere { + Authorization.oidcIdTokenValue eq token + }.orWhere { + Authorization.refreshTokenValue eq token + }.orWhere { + Authorization.userCodeValue eq token + }.orWhere { + Authorization.deviceCodeValue eq token + } + } + + OAuth2ParameterNames.STATE -> { + Authorization.select { Authorization.state eq token } + } + + OAuth2ParameterNames.CODE -> { + Authorization.select { Authorization.authorizationCodeValue eq token } + } + + OAuth2ParameterNames.ACCESS_TOKEN -> { + Authorization.select { Authorization.accessTokenValue eq token } + } + + OidcParameterNames.ID_TOKEN -> { + Authorization.select { Authorization.oidcIdTokenValue eq token } + } + + OAuth2ParameterNames.REFRESH_TOKEN -> { + Authorization.select { Authorization.refreshTokenValue eq token } + } + + OAuth2ParameterNames.USER_CODE -> { + Authorization.select { Authorization.userCodeValue eq token } + } + + OAuth2ParameterNames.DEVICE_CODE -> { + Authorization.select { Authorization.deviceCodeValue eq token } + } + + else -> { + null + } + }?.singleOrNull()?.toAuthorization() + } + + fun ResultRow.toAuthorization(): OAuth2Authorization { + + val registeredClientId = this[Authorization.registeredClientId] + + val registeredClient = registeredClientRepository.findById(registeredClientId) + + val builder = OAuth2Authorization.withRegisteredClient(registeredClient) + val id = this[Authorization.id] + val principalName = this[Authorization.principalName] + val authorizationGrantType = this[Authorization.authorizationGrantType] + val authorizedScopes = this[Authorization.authorizedScopes]?.split(",").orEmpty().toSet() + val attributes = this[Authorization.attributes]?.let { JsonUtil.jsonToMap(it) } ?: emptyMap() + + builder.id(id).principalName(principalName) + .authorizationGrantType(AuthorizationGrantType(authorizationGrantType)).authorizedScopes(authorizedScopes) + .attributes { it.putAll(attributes) } + + val state = this[Authorization.state].orEmpty() + if (state.isNotBlank()) { + builder.attribute(OAuth2ParameterNames.STATE, state) + } + + val authorizationCodeValue = this[Authorization.authorizationCodeValue] + if (authorizationCodeValue.isNullOrBlank()) { + val authorizationCodeIssuedAt = this[Authorization.authorizationCodeIssuedAt] + val authorizationCodeExpiresAt = this[Authorization.authorizationCodeExpiresAt] + val authorizationCodeMetadata = this[Authorization.authorizationCodeMetadata]?.let { + JsonUtil.jsonToMap( + it + ) + } ?: emptyMap() + val oAuth2AuthorizationCode = + OAuth2AuthorizationCode(authorizationCodeValue, authorizationCodeIssuedAt, authorizationCodeExpiresAt) + builder.token(oAuth2AuthorizationCode) { + it.putAll(authorizationCodeMetadata) + } + } + + val accessTokenValue = this[Authorization.accessTokenValue].orEmpty() + if (accessTokenValue.isNotBlank()) { + val accessTokenIssuedAt = this[Authorization.accessTokenIssuedAt] + val accessTokenExpiresAt = this[Authorization.accessTokenExpiresAt] + val accessTokenMetadata = + this[Authorization.accessTokenMetadata]?.let { JsonUtil.jsonToMap(it) } ?: emptyMap() + val accessTokenType = + if (this[Authorization.accessTokenType].equals(OAuth2AccessToken.TokenType.BEARER.value, true)) { + OAuth2AccessToken.TokenType.BEARER + } else { + null + } + + val accessTokenScope = this[Authorization.accessTokenScopes]?.split(",").orEmpty().toSet() + + val oAuth2AccessToken = OAuth2AccessToken( + accessTokenType, accessTokenValue, accessTokenIssuedAt, accessTokenExpiresAt, accessTokenScope + ) + + builder.token(oAuth2AccessToken) { it.putAll(accessTokenMetadata) } + } + + val oidcIdTokenValue = this[Authorization.oidcIdTokenValue].orEmpty() + if (oidcIdTokenValue.isNotBlank()) { + val oidcTokenIssuedAt = this[Authorization.oidcIdTokenIssuedAt] + val oidcTokenExpiresAt = this[Authorization.oidcIdTokenExpiresAt] + val oidcTokenMetadata = + this[Authorization.oidcIdTokenMetadata]?.let { JsonUtil.jsonToMap(it) } ?: emptyMap() + + val oidcIdToken = OidcIdToken( + oidcIdTokenValue, + oidcTokenIssuedAt, + oidcTokenExpiresAt, + oidcTokenMetadata.getValue(OAuth2Authorization.Token.CLAIMS_METADATA_NAME) as MutableMap? + ) + + builder.token(oidcIdToken) { it.putAll(oidcTokenMetadata) } + } + + val refreshTokenValue = this[Authorization.refreshTokenValue].orEmpty() + if (refreshTokenValue.isNotBlank()) { + val refreshTokenIssuedAt = this[Authorization.refreshTokenIssuedAt] + val refreshTokenExpiresAt = this[Authorization.refreshTokenExpiresAt] + val refreshTokenMetadata = + this[Authorization.refreshTokenMetadata]?.let { JsonUtil.jsonToMap(it) } ?: emptyMap() + + val oAuth2RefreshToken = OAuth2RefreshToken(refreshTokenValue, refreshTokenIssuedAt, refreshTokenExpiresAt) + + builder.token(oAuth2RefreshToken) { it.putAll(refreshTokenMetadata) } + } + + val userCodeValue = this[Authorization.userCodeValue].orEmpty() + if (userCodeValue.isNotBlank()) { + val userCodeIssuedAt = this[Authorization.userCodeIssuedAt] + val userCodeExpiresAt = this[Authorization.userCodeExpiresAt] + val userCodeMetadata = + this[Authorization.userCodeMetadata]?.let { JsonUtil.jsonToMap(it) } ?: emptyMap() + val oAuth2UserCode = OAuth2UserCode(userCodeValue, userCodeIssuedAt, userCodeExpiresAt) + builder.token(oAuth2UserCode) { it.putAll(userCodeMetadata) } + } + + val deviceCodeValue = this[Authorization.deviceCodeValue].orEmpty() + if (deviceCodeValue.isNotBlank()) { + val deviceCodeIssuedAt = this[Authorization.deviceCodeIssuedAt] + val deviceCodeExpiresAt = this[Authorization.deviceCodeExpiresAt] + val deviceCodeMetadata = + this[Authorization.deviceCodeMetadata]?.let { JsonUtil.jsonToMap(it) } ?: emptyMap() + + val oAuth2DeviceCode = OAuth2DeviceCode(deviceCodeValue, deviceCodeIssuedAt, deviceCodeExpiresAt) + builder.token(oAuth2DeviceCode) { it.putAll(deviceCodeMetadata) } + } + + return builder.build() + } +} + +object Authorization : Table("authorization") { + val id = varchar("id", 255) + val registeredClientId = varchar("registered_client_id", 255) + val principalName = varchar("principal_name", 255) + val authorizationGrantType = varchar("authorization_grant_type", 255) + val authorizedScopes = varchar("authorized_scopes", 1000).nullable().default(null) + val attributes = varchar("attributes", 4000).nullable().default(null) + val state = varchar("state", 500).nullable().default(null) + val authorizationCodeValue = varchar("authorization_code_value", 4000).nullable().default(null) + val authorizationCodeIssuedAt = timestamp("authorization_code_issued_at").nullable().default(null) + val authorizationCodeExpiresAt = timestamp("authorization_code_expires_at").nullable().default(null) + val authorizationCodeMetadata = varchar("authorization_code_metadata", 2000).nullable().default(null) + val accessTokenValue = varchar("access_token_value", 4000).nullable().default(null) + val accessTokenIssuedAt = timestamp("access_token_issued_at").nullable().default(null) + val accessTokenExpiresAt = timestamp("access_token_expires_at").nullable().default(null) + val accessTokenMetadata = varchar("access_token_metadata", 2000).nullable().default(null) + val accessTokenType = varchar("access_token_type", 255).nullable().default(null) + val accessTokenScopes = varchar("access_token_scopes", 1000).nullable().default(null) + val refreshTokenValue = varchar("refresh_token_value", 4000).nullable().default(null) + val refreshTokenIssuedAt = timestamp("refresh_token_issued_at").nullable().default(null) + val refreshTokenExpiresAt = timestamp("refresh_token_expires_at").nullable().default(null) + val refreshTokenMetadata = varchar("refresh_token_metadata", 2000).nullable().default(null) + val oidcIdTokenValue = varchar("oidc_id_token_value", 4000).nullable().default(null) + val oidcIdTokenIssuedAt = timestamp("oidc_id_token_issued_at").nullable().default(null) + val oidcIdTokenExpiresAt = timestamp("oidc_id_token_expires_at").nullable().default(null) + val oidcIdTokenMetadata = varchar("oidc_id_token_metadata", 2000).nullable().default(null) + val oidcIdTokenClaims = varchar("oidc_id_token_claims", 2000).nullable().default(null) + val userCodeValue = varchar("user_code_value", 4000).nullable().default(null) + val userCodeIssuedAt = timestamp("user_code_issued_at").nullable().default(null) + val userCodeExpiresAt = timestamp("user_code_expires_at").nullable().default(null) + val userCodeMetadata = varchar("user_code_metadata", 2000).nullable().default(null) + val deviceCodeValue = varchar("device_code_value", 4000).nullable().default(null) + val deviceCodeIssuedAt = timestamp("device_code_issued_at").nullable().default(null) + val deviceCodeExpiresAt = timestamp("device_code_expires_at").nullable().default(null) + val deviceCodeMetadata = varchar("device_code_metadata", 2000).nullable().default(null) + + override val primaryKey = PrimaryKey(id) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/UserDetailsServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/UserDetailsServiceImpl.kt new file mode 100644 index 00000000..a889e79a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/UserDetailsServiceImpl.kt @@ -0,0 +1,26 @@ +package dev.usbharu.hideout.service.auth + +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.core.Transaction +import kotlinx.coroutines.runBlocking +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.stereotype.Service + +@Service +class UserDetailsServiceImpl(private val userQueryService: UserQueryService, private val transaction: Transaction) : + UserDetailsService { + override fun loadUserByUsername(username: String?): UserDetails = runBlocking { + if (username == null) { + throw UsernameNotFoundException("$username not found") + } + transaction.transaction { + val findById = userQueryService.findByNameAndDomain(username, "") + User( + findById.name, findById.password, listOf() + ) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/UsernamePasswordAuthFilter.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/UsernamePasswordAuthFilter.kt new file mode 100644 index 00000000..5655e8b4 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/UsernamePasswordAuthFilter.kt @@ -0,0 +1,17 @@ +package dev.usbharu.hideout.service.auth + +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.util.matcher.AntPathRequestMatcher + + +class UsernamePasswordAuthFilter(jwtService: JwtService, authenticationManager: AuthenticationManager?) : + UsernamePasswordAuthenticationFilter(authenticationManager) { + init { + setRequiresAuthenticationRequestMatcher(AntPathRequestMatcher("/api/internal/v1/login", "POST")) + + this.setAuthenticationSuccessHandler { request, response, authentication -> + + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/JsonUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/JsonUtil.kt new file mode 100644 index 00000000..fcd97a9f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/JsonUtil.kt @@ -0,0 +1,16 @@ +package dev.usbharu.hideout.util + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue + +object JsonUtil { + val objectMapper = jacksonObjectMapper() + + fun mapToJson(map: Map<*, *>, objectMapper: ObjectMapper = this.objectMapper): String = + objectMapper.writeValueAsString(map) + + fun jsonToMap(json: String, objectMapper: ObjectMapper = this.objectMapper): Map = + objectMapper.readValue(json) + +}