mirror of https://github.com/usbharu/Hideout.git
feat: OAuth2でトークンの発行ができるように
This commit is contained in:
parent
a13fe45d0d
commit
97d9bf0898
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>>() {}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue