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 {
if (registerApplication.useRefreshToken) {
authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
} else {
tokenSettings(
TokenSettings
.builder()
@ -84,7 +85,7 @@ class RegisterApplicationApplicationService(
id = id,
name = registerApplication.name,
clientSecret = clientSecret,
clientId = id,
clientId = id.toString(),
redirectUris = registerApplication.redirectUris
)
}

View File

@ -23,5 +23,5 @@ data class RegisteredApplication(
val name: String,
val redirectUris: Set<URI>,
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.crypto.bcrypt.BCryptPasswordEncoder
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.RegisteredClientRepository
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
@Configuration
@EnableWebSecurity(debug = false)
@EnableWebSecurity(debug = true)
class SecurityConfig {
@Bean
fun passwordEncoder(): PasswordEncoder {
@ -82,6 +85,20 @@ class SecurityConfig {
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
fun roleHierarchy(): RoleHierarchy {
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
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.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import java.io.Serial
import java.util.*
class HideoutUserDetails(
private val authorities: MutableList<out GrantedAuthority>,
authorities: Set<GrantedAuthority>,
private val password: String,
private val username: String,
val userDetailsId: Long,
) : UserDetails {
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
private val authorities: MutableSet<GrantedAuthority> = Collections.unmodifiableSet(authorities)
override fun getAuthorities(): MutableSet<GrantedAuthority> {
return authorities
}
@ -36,4 +51,79 @@ class HideoutUserDetails(
override fun getUsername(): String {
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)
?: throw UsernameNotFoundException("${actor.id.id} not found")
HideoutUserDetails(
authorities = mutableListOf(),
authorities = HashSet(),
password = userDetail.password.password,
actor.name.name,
userDetailsId = userDetail.id.id

View File

@ -55,7 +55,7 @@ create table if not exists actors
suspend boolean not null,
move_to bigint null default null,
emojis varchar(3000) not null default '',
deleted boolean not null default false,
deleted boolean not null default false,
unique ("name", "domain"),
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
@ -185,3 +185,49 @@ create table if not exists oauth2_registered_client
token_settings varchar(2000) NOT NULL,
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>
</Appenders>
<Loggers>
<Root level="DEBUG">
<Root level="TRACE">
<AppenderRef ref="Console"/>
</Root>
<Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/>

View File

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