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 {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -23,5 +23,5 @@ data class RegisteredApplication(
|
|||
val name: String,
|
||||
val redirectUris: Set<URI>,
|
||||
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.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(
|
||||
|
|
|
@ -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
|
||||
|
||||
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>>() {}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue