feat: OAuth2でトークンの発行ができるように

This commit is contained in:
usbharu 2024-06-07 13:05:55 +09:00
parent a13fe45d0d
commit 97d9bf0898
9 changed files with 210 additions and 10 deletions

View File

@ -61,6 +61,7 @@ class RegisterApplicationApplicationService(
.apply { .apply {
if (registerApplication.useRefreshToken) { if (registerApplication.useRefreshToken) {
authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
} else {
tokenSettings( tokenSettings(
TokenSettings TokenSettings
.builder() .builder()
@ -84,7 +85,7 @@ class RegisterApplicationApplicationService(
id = id, id = id,
name = registerApplication.name, name = registerApplication.name,
clientSecret = clientSecret, clientSecret = clientSecret,
clientId = id, clientId = id.toString(),
redirectUris = registerApplication.redirectUris redirectUris = registerApplication.redirectUris
) )
} }

View File

@ -23,5 +23,5 @@ data class RegisteredApplication(
val name: String, val name: String,
val redirectUris: Set<URI>, val redirectUris: Set<URI>,
val clientSecret: String, val clientSecret: String,
val clientId: Long, val clientId: String,
) )

View File

@ -29,14 +29,17 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.annotation.web.invoke import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository 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.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration 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.SecurityFilterChain
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
@Configuration @Configuration
@EnableWebSecurity(debug = false) @EnableWebSecurity(debug = true)
class SecurityConfig { class SecurityConfig {
@Bean @Bean
fun passwordEncoder(): PasswordEncoder { fun passwordEncoder(): PasswordEncoder {
@ -82,6 +85,20 @@ class SecurityConfig {
return JdbcRegisteredClientRepository(jdbcOperations) return JdbcRegisteredClientRepository(jdbcOperations)
} }
@Bean
fun oauth2AuthorizationConsentService(
jdbcOperations: JdbcOperations,
registeredClientRepository: RegisteredClientRepository,
): OAuth2AuthorizationConsentService {
return JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository)
}
@Bean
fun authorizationServerSettings(): AuthorizationServerSettings {
return AuthorizationServerSettings.builder().authorizationEndpoint("/oauth/authorize")
.tokenEndpoint("/oauth/token").tokenRevocationEndpoint("/oauth/revoke").build()
}
@Bean @Bean
fun roleHierarchy(): RoleHierarchy { fun roleHierarchy(): RoleHierarchy {
val roleHierarchyImpl = RoleHierarchyImpl.fromHierarchy( val roleHierarchyImpl = RoleHierarchyImpl.fromHierarchy(

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.JdbcOperations
import org.springframework.jdbc.support.lob.DefaultLobHandler
import org.springframework.jdbc.support.lob.LobHandler
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.stereotype.Component
@Component
class HideoutJdbcOauth2AuthorizationService(
registeredClientRepository: RegisteredClientRepository,
jdbcOperations: JdbcOperations,
@Autowired(required = false) lobHandler: LobHandler = DefaultLobHandler(),
) : JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository, lobHandler) {
init {
super.setAuthorizationRowMapper(HideoutOAuth2AuthorizationRowMapper(registeredClientRepository = registeredClientRepository))
}
class HideoutOAuth2AuthorizationRowMapper(registeredClientRepository: RegisteredClientRepository?) :
OAuth2AuthorizationRowMapper(registeredClientRepository) {
init {
objectMapper.addMixIn(HideoutUserDetails::class.java, UserDetailsMixin::class.java)
}
}
}

View File

@ -16,16 +16,31 @@
package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetails
import java.io.Serial
import java.util.*
class HideoutUserDetails( class HideoutUserDetails(
private val authorities: MutableList<out GrantedAuthority>, authorities: Set<GrantedAuthority>,
private val password: String, private val password: String,
private val username: String, private val username: String,
val userDetailsId: Long, val userDetailsId: Long,
) : UserDetails { ) : UserDetails {
override fun getAuthorities(): MutableCollection<out GrantedAuthority> { private val authorities: MutableSet<GrantedAuthority> = Collections.unmodifiableSet(authorities)
override fun getAuthorities(): MutableSet<GrantedAuthority> {
return authorities return authorities
} }
@ -36,4 +51,79 @@ class HideoutUserDetails(
override fun getUsername(): String { override fun getUsername(): String {
return username return username
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as HideoutUserDetails
if (authorities != other.authorities) return false
if (password != other.password) return false
if (username != other.username) return false
if (userDetailsId != other.userDetailsId) return false
return true
}
override fun hashCode(): Int {
var result = authorities.hashCode()
result = 31 * result + password.hashCode()
result = 31 * result + username.hashCode()
result = 31 * result + userDetailsId.hashCode()
return result
}
override fun toString(): String {
return "HideoutUserDetails(authorities=$authorities, password='$password', username='$username', userDetailsId=$userDetailsId)"
}
companion object {
@Serial
private const val serialVersionUID = -899168205656607781L
}
}
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonDeserialize(using = UserDetailsDeserializer::class)
@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
creatorVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonSubTypes
@Suppress("UnnecessaryAbstractClass")
abstract class UserDetailsMixin
class UserDetailsDeserializer : JsonDeserializer<HideoutUserDetails>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): HideoutUserDetails {
val mapper = p.codec as ObjectMapper
val jsonNode: JsonNode = mapper.readTree(p)
val authorities: Set<GrantedAuthority> = mapper.convertValue(
jsonNode["authorities"],
SIMPLE_GRANTED_AUTHORITY_SET
)
val password = jsonNode.readText("password")
return HideoutUserDetails(
userDetailsId = jsonNode["userDetailsId"].longValue(),
username = jsonNode.readText("username"),
password = password,
authorities = authorities
)
}
fun JsonNode.readText(field: String, defaultValue: String = ""): String {
return when {
has(field) -> get(field).asText(defaultValue)
else -> defaultValue
}
}
companion object {
private val SIMPLE_GRANTED_AUTHORITY_SET = object : TypeReference<Set<SimpleGrantedAuthority>>() {}
}
} }

View File

@ -43,7 +43,7 @@ class UserDetailsServiceImpl(
val userDetail = userDetailRepository.findByActorId(actor.id.id) val userDetail = userDetailRepository.findByActorId(actor.id.id)
?: throw UsernameNotFoundException("${actor.id.id} not found") ?: throw UsernameNotFoundException("${actor.id.id} not found")
HideoutUserDetails( HideoutUserDetails(
authorities = mutableListOf(), authorities = HashSet(),
password = userDetail.password.password, password = userDetail.password.password,
actor.name.name, actor.name.name,
userDetailsId = userDetail.id.id userDetailsId = userDetail.id.id

View File

@ -55,7 +55,7 @@ create table if not exists actors
suspend boolean not null, suspend boolean not null,
move_to bigint null default null, move_to bigint null default null,
emojis varchar(3000) not null default '', emojis varchar(3000) not null default '',
deleted boolean not null default false, deleted boolean not null default false,
unique ("name", "domain"), unique ("name", "domain"),
constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict, constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict,
constraint fk_actors_actors__move_to foreign key ("move_to") references actors (id) on delete restrict on update restrict constraint fk_actors_actors__move_to foreign key ("move_to") references actors (id) on delete restrict on update restrict
@ -185,3 +185,49 @@ create table if not exists oauth2_registered_client
token_settings varchar(2000) NOT NULL, token_settings varchar(2000) NOT NULL,
PRIMARY KEY (id) PRIMARY KEY (id)
); );
CREATE TABLE if not exists oauth2_authorization_consent
(
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorities varchar(1000) NOT NULL,
PRIMARY KEY (registered_client_id, principal_name)
);
CREATE TABLE oauth2_authorization
(
id varchar(100) NOT NULL,
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorization_grant_type varchar(100) NOT NULL,
authorized_scopes varchar(1000) DEFAULT NULL,
attributes varchar(4000) DEFAULT NULL,
state varchar(500) DEFAULT NULL,
authorization_code_value varchar(4000) DEFAULT NULL,
authorization_code_issued_at timestamp DEFAULT NULL,
authorization_code_expires_at timestamp DEFAULT NULL,
authorization_code_metadata varchar(4000) DEFAULT NULL,
access_token_value varchar(4000) DEFAULT NULL,
access_token_issued_at timestamp DEFAULT NULL,
access_token_expires_at timestamp DEFAULT NULL,
access_token_metadata varchar(4000) DEFAULT NULL,
access_token_type varchar(100) DEFAULT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
oidc_id_token_value varchar(4000) DEFAULT NULL,
oidc_id_token_issued_at timestamp DEFAULT NULL,
oidc_id_token_expires_at timestamp DEFAULT NULL,
oidc_id_token_metadata varchar(4000) DEFAULT NULL,
refresh_token_value varchar(4000) DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
refresh_token_expires_at timestamp DEFAULT NULL,
refresh_token_metadata varchar(4000) DEFAULT NULL,
user_code_value varchar(4000) DEFAULT NULL,
user_code_issued_at timestamp DEFAULT NULL,
user_code_expires_at timestamp DEFAULT NULL,
user_code_metadata varchar(4000) DEFAULT NULL,
device_code_value varchar(4000) DEFAULT NULL,
device_code_issued_at timestamp DEFAULT NULL,
device_code_expires_at timestamp DEFAULT NULL,
device_code_metadata varchar(4000) DEFAULT NULL,
PRIMARY KEY (id)
);

View File

@ -6,7 +6,7 @@
</Console> </Console>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="DEBUG"> <Root level="TRACE">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
</Root> </Root>
<Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/> <Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/>

View File

@ -33,7 +33,7 @@ class SpringAppApi(private val registerApplicationApplicationService: RegisterAp
appsRequest.clientName, appsRequest.clientName,
setOf(URI.create(appsRequest.redirectUris)), setOf(URI.create(appsRequest.redirectUris)),
false, false,
appsRequest.scopes?.split(" ").orEmpty().toSet() appsRequest.scopes?.split(" ").orEmpty().toSet().ifEmpty { setOf("read") }
) )
val registeredApplication = registerApplicationApplicationService.register(registerApplication) val registeredApplication = registerApplicationApplicationService.register(registerApplication)
return ResponseEntity.ok( return ResponseEntity.ok(
@ -41,7 +41,7 @@ class SpringAppApi(private val registerApplicationApplicationService: RegisterAp
registeredApplication.name, registeredApplication.name,
"invalid-vapid-key", "invalid-vapid-key",
null, null,
registeredApplication.clientId.toString(), registeredApplication.clientId,
registeredApplication.clientSecret, registeredApplication.clientSecret,
appsRequest.redirectUris appsRequest.redirectUris
) )