144 val nonce = formParameters["nonce"]
145
146 // パラメータのバリデーション
147 if (username == null || password == null || responseType != "code" || clientId == null || redirectUri == null || scope == null || state == null || codeChallenge == null) {
148 // redirect_uriとstateが存在する場合のみエラーリダイレクト
149 if (redirectUri != null && state != null) {
150 val errorUri = buildErrorRedirectUri(redirectUri, "invalid_request", "It does not have the required parameters", state)
37 * @param processingTypes 処理中の型を追跡するセット(スレッドセーフのため関数ローカル)
38 * @return 生成されたスキーマ
39 */
40 private fun generateSchemaInternal(type: KType, processingTypes: MutableSet<KClass<*>>): Schema {
41 val classifier = type.classifier as? KClass<*>
42 ?: return Schema(type = "object")
43
82 }
83 }
84
85 internal fun Application.module() {
86 val plugin: MineAuth by inject(MineAuth::class.java)
87 val jwtConfigData: JWTConfigData = get(JWTConfigData::class.java)
88 val mineAuthConfig: MineAuthConfig = get(MineAuthConfig::class.java)
39 * GET /authorize: 認可画面を表示
40 * POST /authorize: 認可リクエストを処理
41 */
42 fun Route.authorizeRouter() {
43 // 認可画面を表示するエンドポイント
44 get("/authorize") {
45 // OAuth2.0の必須パラメータを取得
32 // RFC 6749 Section 4.1.2: 認可コードの最大有効期間(10分推奨)
33 private const val AUTHORIZATION_CODE_LIFETIME_MS = 10 * 60 * 1000L
34
35 fun Route.tokenRouter() {
36 post("/token") {
37 val formParameters = call.receiveParameters()
38 val grantType = formParameters["grant_type"]
330 * @param accessToken アクセストークン(at_hash計算用)
331 * @return ID Token(JWT形式)
332 */
333 private suspend fun issueIdToken(
334 data: AuthorizedData,
335 clientId: String?,
336 accessToken: String
146 /**
147 * OAuth2/OIDCエンドポイントを生成する
148 */
149 private fun generateOAuthPaths(): Map<String, PathItem> {
150 // トークンレスポンスのスキーマ
151 val tokenResponseSchema = Schema(
152 type = "object",
82 }
83 }
84
85 internal fun Application.module() {
86 val plugin: MineAuth by inject(MineAuth::class.java)
87 val jwtConfigData: JWTConfigData = get(JWTConfigData::class.java)
88 val mineAuthConfig: MineAuthConfig = get(MineAuthConfig::class.java)
39 * GET /authorize: 認可画面を表示
40 * POST /authorize: 認可リクエストを処理
41 */
42 fun Route.authorizeRouter() {
43 // 認可画面を表示するエンドポイント
44 get("/authorize") {
45 // OAuth2.0の必須パラメータを取得
32 // RFC 6749 Section 4.1.2: 認可コードの最大有効期間(10分推奨)
33 private const val AUTHORIZATION_CODE_LIFETIME_MS = 10 * 60 * 1000L
34
35 fun Route.tokenRouter() {
36 post("/token") {
37 val formParameters = call.receiveParameters()
38 val grantType = formParameters["grant_type"]
22 * Ktorのコンテキストからパラメータ値を解決するクラス
23 * 各パラメータタイプに対応した解決ロジックを提供する
24 */
25 class ParameterResolver(
26 private val json: Json = Json { ignoreUnknownKeys = true },
27 private val maxBodySize: Int = MAX_BODY_SIZE
28 ) : KoinComponent {
65 * OAuthクライアントリポジトリ
66 * OAuth2/OIDCクライアントのCRUD操作を提供する
67 */
68 object OAuthClientRepository {
69 // UUIDv7生成器(時間ソート可能なUUID)
70 private val uuidGenerator = Generators.timeBasedEpochGenerator()
71
40 * サービスアカウントトークンリポジトリ
41 * ServiceAccountTokensテーブルに対するCRUD操作を提供する
42 */
43 object ServiceAccountTokenRepository {
44
45 /**
46 * 新しいトークンメタデータを保存する
12 import java.security.MessageDigest
13 import java.util.*
14
15 object OAuthValidation : KoinComponent {
16
17 /**
18 * クライアントIDでクライアントデータを取得・検証する
2
3 @Target(AnnotationTarget.VALUE_PARAMETER)
4 @Retention(AnnotationRetention.RUNTIME)
5 annotation class AccessUser()
6
2
3 @Target(AnnotationTarget.VALUE_PARAMETER)
4 @Retention(AnnotationRetention.RUNTIME)
5 annotation class AuthedAccessUser()
6
6 */
7 @Target(AnnotationTarget.VALUE_PARAMETER)
8 @Retention(AnnotationRetention.RUNTIME)
9 annotation class QueryParams()
10
3 import io.ktor.server.routing.*
4
5 object UserRouter {
6 fun Route.userRouter() {
7
8 }
9 }
111 val parsed = URI(uri)
112 // スキームとホストが存在することを確認
113 parsed.scheme != null && parsed.host != null
114 } catch (e: Exception) {
115 false
116 }
117 }
39 } catch (e: InvocationTargetException) {
40 // ラップされた例外を取り出して処理する
41 handleInvocationTargetException(e)
42 } catch (e: IllegalArgumentException) {
43 // 引数型の不一致エラー
44 ExecutionError.ArgumentTypeMismatch(
45 methodName = metadata.method.name,
44 } catch (e: InvocationTargetException) {
45 // ラップされた例外を取り出して処理する
46 handleInvocationTargetException(e)
47 } catch (e: IllegalArgumentException) {
48 // 引数型の不一致エラー
49 ExecutionError.ArgumentTypeMismatch(
50 methodName = metadata.method.name,
52 val uuidStr = principal.payload.getClaim("playerUniqueId").asString() ?: return null
53 return try {
54 UUID.fromString(uuidStr)
55 } catch (e: IllegalArgumentException) {
56 null
57 }
58 }
57 }
58 try {
59 UUID.fromString(uuidStr)
60 } catch (e: IllegalArgumentException) {
61 raise(ResolveError.AuthenticationRequired("Invalid UUID format"))
62 }
63 }
61 UUID::class -> UUID.fromString(value)
62 else -> value // デフォルトは文字列として返す
63 }
64 } catch (e: IllegalArgumentException) {
65 raise(
66 ResolveError.TypeConversionFailed(
67 parameterName = paramName,
51 try {
52 val uuid = UUID.fromString(uuidStr)
53 AuthResult.PlayerAuth(uuid)
54 } catch (e: IllegalArgumentException) {
55 raise(AuthError.InvalidToken("Invalid UUID format: $uuidStr"))
56 }
57 }
199 UUID::class -> UUID.fromString(value)
200 else -> value // デフォルトは文字列として返す
201 }
202 } catch (e: IllegalArgumentException) {
203 raise(
204 ResolveError.TypeConversionFailed(
205 parameterName = paramName,
223 }
224 try {
225 UUID.fromString(uuidStr)
226 } catch (e: IllegalArgumentException) {
227 raise(ResolveError.AuthenticationRequired("Invalid UUID format"))
228 }
229 }
238 val uuidStr = principal.payload.getClaim("playerUniqueId").asString() ?: return null
239 return try {
240 UUID.fromString(uuidStr)
241 } catch (e: IllegalArgumentException) {
242 null
243 }
244 }
350 return try {
351 UUID.fromString(value)
352 true
353 } catch (e: IllegalArgumentException) {
354 false
355 }
356 }
358 OAuthClients.selectAll()
359 .map { it[OAuthClients.clientId] }
360 }
361 } catch (e: Exception) {
362 emptyList()
363 }
364 }
96 RevokedTokens.selectAll()
97 .where { RevokedTokens.tokenId eq tokenId }
98 .firstOrNull() != null
99 } catch (e: Exception) {
100 // エラー時は安全側に倒して失効済みとして扱う
101 true
102 }
113 RevokedTokens.deleteWhere {
114 expiresAt less LocalDateTime.now()
115 }
116 } catch (e: Exception) {
117 0
118 }
119 }
226 .firstOrNull()
227
228 row != null && !row[ServiceAccountTokens.revoked]
229 } catch (e: Exception) {
230 false
231 }
232 }
89 // JWT.decode()を使わないことで、署名未検証のJWTからクレームを抽出するリスクを排除
90 val jwt = try {
91 JwtProvider.lenientVerifier.verify(idTokenHint)
92 } catch (e: JWTVerificationException) {
93 // 署名不正・issuer不一致 → 無効
94 return null
95 }
112 private fun validatePostLogoutRedirectUri(clientId: String, postLogoutRedirectUri: String): Boolean {
113 val clientData = try {
114 ClientData.getClientData(clientId)
115 } catch (e: Exception) {
116 return false
117 }
118
78 // 署名・有効期限を検証(JwtProviderのキャッシュ済みverifierを使用)
79 val jwt = try {
80 JwtProvider.verifier.verify(token)
81 } catch (e: TokenExpiredException) {
82 // 署名は有効だが期限切れ → inactive
83 return IntrospectionResponse(active = false)
84 } catch (e: JWTVerificationException) {
81 } catch (e: TokenExpiredException) {
82 // 署名は有効だが期限切れ → inactive
83 return IntrospectionResponse(active = false)
84 } catch (e: JWTVerificationException) {
85 // 署名不正・issuer不一致 → inactive
86 return IntrospectionResponse(active = false)
87 }
101 val username = playerUniqueId?.let {
102 try {
103 org.bukkit.Bukkit.getOfflinePlayer(java.util.UUID.fromString(it)).name
104 } catch (e: Exception) {
105 null
106 }
107 }
53
54 return try {
55 Regex(pattern).matches(redirectUri)
56 } catch (e: Exception) {
57 false
58 }
59 }
142 val id = decoded.substring(0, colonIndex)
143 val secret = decoded.substring(colonIndex + 1)
144 Pair(id, secret)
145 } catch (e: IllegalArgumentException) {
146 null
147 }
148 }
205 // クライアントデータをDBから取得
206 val clientData = try {
207 ClientData.getClientData(credentials.clientId)
208 } catch (e: Exception) {
209 return ClientAuthFailure(OAuthErrorCode.INVALID_CLIENT, "Client not found").left()
210 }
211
238 // クライアントデータをDBから取得
239 val clientData = try {
240 ClientData.getClientData(credentials.clientId)
241 } catch (e: Exception) {
242 return ClientAuthFailure(OAuthErrorCode.INVALID_CLIENT, "Client not found").left()
243 }
244
103 // JWTVerificationException: 署名不正等 → 成功として扱う(情報漏洩防止)
104 val jwt: DecodedJWT = try {
105 JwtProvider.lenientVerifier.verify(token)
106 } catch (e: JWTVerificationException) {
107 // 署名不正・issuer不一致などは失効対象外だが、成功として扱う
108 return true
109 }
276 tokenId = tokenId,
277 expiresAt = expiresAt
278 )
279 } catch (e: Exception) {
280 // 署名無効、有効期限切れ、その他のJWTエラー
281 null
282 }
71 config.exporters.forEach { exporter ->
72 val endpointHost = try {
73 URI(exporter.endpoint).host ?: "unknown"
74 } catch (e: Exception) {
75 "invalid-endpoint"
76 }
77 plugin.logger.info("Initializing OpenTelemetry ${exporter.protocol} exporter for: $endpointHost")
111 val parsed = URI(uri)
112 // スキームとホストが存在することを確認
113 parsed.scheme != null && parsed.host != null
114 } catch (e: Exception) {
115 false
116 }
117 }
46 expectedTypes = javaMethod.parameterTypes.map { it.simpleName },
47 actualTypes = resolvedParams.map { it?.javaClass?.simpleName ?: "null" }
48 ).left()
49 } catch (e: Exception) {
50 ExecutionError.UnexpectedError(
51 message = e.message ?: "Unknown error",
52 cause = e
51 expectedTypes = javaMethod.parameterTypes.map { it.simpleName },
52 actualTypes = resolvedParams.map { it?.javaClass?.simpleName ?: "null" }
53 ).left()
54 } catch (e: Exception) {
55 ExecutionError.UnexpectedError(
56 message = e.message ?: "Unknown error",
57 cause = e
32 val serializer = serializer(bodyParam.type)
33 // デシリアライズして返す
34 json.decodeFromString(serializer, bodyText) as Any
35 } catch (e: Exception) {
36 raise(ResolveError.InvalidBodyFormat(e))
37 }
38 }
128 } catch (e: CancellationException) {
129 // コルーチンのキャンセルは再送出して適切に伝播させる
130 throw e
131 } catch (e: Exception) {
132 raise(ResolveError.InvalidBodyFormat(e))
133 }
134 }
87 accountType = accountType,
88 identifier = identifier
89 ).right()
90 } catch (e: Exception) {
91 AccountError.DatabaseError(e.message ?: "Unknown error").left()
92 }
93 }
109 } else {
110 row.toAccountData().right()
111 }
112 } catch (e: Exception) {
113 AccountError.DatabaseError(e.message ?: "Unknown error").left()
114 }
115 }
135 } else {
136 row.toAccountData().right()
137 }
138 } catch (e: Exception) {
139 AccountError.DatabaseError(e.message ?: "Unknown error").left()
140 }
141 }
171 .where { Accounts.accountType eq AccountType.SERVICE.value }
172 .map { it.toAccountData() }
173 rows.right()
174 } catch (e: Exception) {
175 AccountError.DatabaseError(e.message ?: "Unknown error").left()
176 }
177 }
197 } else {
198 Unit.right()
199 }
200 } catch (e: Exception) {
201 AccountError.DatabaseError(e.message ?: "Unknown error").left()
202 }
203 }
142 ),
143 clientSecret = plainSecret
144 ).right()
145 } catch (e: Exception) {
146 OAuthClientError.DatabaseError(e.message ?: "Unknown error").left()
147 }
148 }
164 } else {
165 row.toOAuthClientData().right()
166 }
167 } catch (e: Exception) {
168 OAuthClientError.DatabaseError(e.message ?: "Unknown error").left()
169 }
170 }
206 }
207
208 row.toOAuthClientData().right()
209 } catch (e: Exception) {
210 OAuthClientError.DatabaseError(e.message ?: "Unknown error").left()
211 }
212 }
245 .first()
246
247 updated.toOAuthClientData().right()
248 } catch (e: Exception) {
249 OAuthClientError.DatabaseError(e.message ?: "Unknown error").left()
250 }
251 }
281 }
282
283 newSecret.right()
284 } catch (e: Exception) {
285 OAuthClientError.DatabaseError(e.message ?: "Unknown error").left()
286 }
287 }
300 .map { it.toOAuthClientData() }
301
302 clients.right()
303 } catch (e: Exception) {
304 OAuthClientError.DatabaseError(e.message ?: "Unknown error").left()
305 }
306 }
325 // 削除実行
326 OAuthClients.deleteWhere { OAuthClients.clientId eq clientId }
327 Unit.right()
328 } catch (e: Exception) {
329 OAuthClientError.DatabaseError(e.message ?: "Unknown error").left()
330 }
331 }
341 .map { it.toOAuthClientData() }
342
343 clients.right()
344 } catch (e: Exception) {
345 OAuthClientError.DatabaseError(e.message ?: "Unknown error").left()
346 }
347 }
358 OAuthClients.selectAll()
359 .map { it[OAuthClients.clientId] }
360 }
361 } catch (e: Exception) {
362 emptyList()
363 }
364 }
80 }
81
82 Unit.right()
83 } catch (e: Exception) {
84 RevokedTokenError.DatabaseError(e.message ?: "Unknown error").left()
85 }
86 }
96 RevokedTokens.selectAll()
97 .where { RevokedTokens.tokenId eq tokenId }
98 .firstOrNull() != null
99 } catch (e: Exception) {
100 // エラー時は安全側に倒して失効済みとして扱う
101 true
102 }
113 RevokedTokens.deleteWhere {
114 expiresAt less LocalDateTime.now()
115 }
116 } catch (e: Exception) {
117 0
118 }
119 }
74 lastUsedAt = null,
75 revoked = false
76 ).right()
77 } catch (e: Exception) {
78 ServiceAccountTokenError.DatabaseError(e.message ?: "Unknown error").left()
79 }
80 }
94 } else {
95 row.toTokenData().right()
96 }
97 } catch (e: Exception) {
98 ServiceAccountTokenError.DatabaseError(e.message ?: "Unknown error").left()
99 }
100 }
109 .where { ServiceAccountTokens.accountId eq accountId }
110 .map { it.toTokenData() }
111 rows.right()
112 } catch (e: Exception) {
113 ServiceAccountTokenError.DatabaseError(e.message ?: "Unknown error").left()
114 }
115 }
126 it[revoked] = true
127 }
128 count.right()
129 } catch (e: Exception) {
130 ServiceAccountTokenError.DatabaseError(e.message ?: "Unknown error").left()
131 }
132 }
142 ServiceAccountTokens.accountId eq accountId
143 }
144 count.right()
145 } catch (e: Exception) {
146 ServiceAccountTokenError.DatabaseError(e.message ?: "Unknown error").left()
147 }
148 }
188 lastUsedAt = null,
189 revoked = false
190 ).right()
191 } catch (e: Exception) {
192 // トランザクション全体がロールバックされる
193 ServiceAccountTokenError.DatabaseError(e.message ?: "Unknown error").left()
194 }
211 } else {
212 Unit.right()
213 }
214 } catch (e: Exception) {
215 ServiceAccountTokenError.DatabaseError(e.message ?: "Unknown error").left()
216 }
217 }
226 .firstOrNull()
227
228 row != null && !row[ServiceAccountTokens.revoked]
229 } catch (e: Exception) {
230 false
231 }
232 }
250 it[lastUsedAt] = LocalDateTime.now()
251 }
252 Unit.right()
253 } catch (e: Exception) {
254 ServiceAccountTokenError.DatabaseError(e.message ?: "Unknown error").left()
255 }
256 }
96
97 // audクレームからクライアントIDを取得
98 jwt.audience?.firstOrNull()
99 } catch (e: Exception) {
100 plugin.logger.warning("ID token hint validation error: ${e.message}")
101 null
102 }
112 private fun validatePostLogoutRedirectUri(clientId: String, postLogoutRedirectUri: String): Boolean {
113 val clientData = try {
114 ClientData.getClientData(clientId)
115 } catch (e: Exception) {
116 return false
117 }
118
101 val username = playerUniqueId?.let {
102 try {
103 org.bukkit.Bukkit.getOfflinePlayer(java.util.UUID.fromString(it)).name
104 } catch (e: Exception) {
105 null
106 }
107 }
120 iss = jwt.issuer,
121 jti = jwt.id
122 )
123 } catch (e: Exception) {
124 plugin.logger.warning("Token introspection error: ${e.message}")
125 IntrospectionResponse(active = false)
126 }
53
54 return try {
55 Regex(pattern).matches(redirectUri)
56 } catch (e: Exception) {
57 false
58 }
59 }
205 // クライアントデータをDBから取得
206 val clientData = try {
207 ClientData.getClientData(credentials.clientId)
208 } catch (e: Exception) {
209 return ClientAuthFailure(OAuthErrorCode.INVALID_CLIENT, "Client not found").left()
210 }
211
238 // クライアントデータをDBから取得
239 val clientData = try {
240 ClientData.getClientData(credentials.clientId)
241 } catch (e: Exception) {
242 return ClientAuthFailure(OAuthErrorCode.INVALID_CLIENT, "Client not found").left()
243 }
244
113 } catch (e: CancellationException) {
114 // コルーチンのキャンセルは再送出する(握り潰してはいけない)
115 throw e
116 } catch (e: Exception) {
117 plugin.logger.warning("Token revocation error: ${e.message}")
118 // 内部エラーは成功として扱う(情報漏洩防止)
119 true
276 tokenId = tokenId,
277 expiresAt = expiresAt
278 )
279 } catch (e: Exception) {
280 // 署名無効、有効期限切れ、その他のJWTエラー
281 null
282 }
303 )
304 val result = RevokedTokenRepository.revoke(tokenId, TokenType.REFRESH_TOKEN, clientId, expiresAtLocal)
305 result.isRight()
306 } catch (e: Exception) {
307 plugin.logger.warning("Failed to revoke old refresh token: ${e.message}")
308 false
309 }
71 config.exporters.forEach { exporter ->
72 val endpointHost = try {
73 URI(exporter.endpoint).host ?: "unknown"
74 } catch (e: Exception) {
75 "invalid-endpoint"
76 }
77 plugin.logger.info("Initializing OpenTelemetry ${exporter.protocol} exporter for: $endpointHost")
1 package party.morino.mineauth.api.model.common
2
3 import kotlinx.serialization.Serializable
4 import party.morino.mineauth.api.utils.UUIDSerializer
22 * @property coroutineContext コルーチン実行に使用するコンテキスト
23 * @property enableClassLoaderBridging クラスローダー間の互換性のための動的プロキシを有効にするか
24 */
25 data class KtorSupportConfig(
26 val coroutineScope: CoroutineScope = GlobalScope,
27 val coroutineContext: CoroutineContext = EmptyCoroutineContext,
28 val enableClassLoaderBridging: Boolean = true
16
17
18 // OfflinePlayer <==> UUID
19 object OfflinePlayerSerializer : KSerializer<OfflinePlayer> {
20 override val descriptor = PrimitiveSerialDescriptor("OfflinePlayer", PrimitiveKind.STRING)
21
22 override fun deserialize(decoder: Decoder): OfflinePlayer {
48 ): Iterable<String> = OAuthClientRepository.getAllClientIdsBlocking()
49
50 companion object {
51 /**
52 * ParserDescriptorを生成するファクトリメソッド
53 */
54 fun clientIdParser(): ParserDescriptor<CommandSender, ClientId> =
59 }
60
61 companion object {
62 /**
63 * ParserDescriptorを生成するファクトリメソッド
64 */
65 fun serviceNameParser(): ParserDescriptor<CommandSender, ServiceName> =
15 */
16 object OpenApiRouter {
17
18 /**
19 * OpenAPI関連のルートを設定する
20 */
21 fun Route.openApiRouter() {
6 import party.morino.mineauth.core.web.router.auth.oauth.OAuthRouter.oauthRouter
7
8 object AuthRouter {
9 fun Route.authRouter() {
10 wellKnownRouter()
11 oauthRouter()
12 loginRouter()
5 import io.ktor.server.velocity.*
6
7 object LoginRouter {
8 fun Route.loginRouter() {
9 route("/login") {
10 get { // 認証画面を返す
11 val model = mapOf<String, String>()
17 object WellKnownRouter : KoinComponent {
18 private val config: MineAuthConfig by inject()
19
20 fun Route.wellKnownRouter() {
21 route(".well-known") {
22 // OIDC Discovery Endpoint
23 // OpenID Connect Discovery 1.0 準拠
34 private const val CODE_LENGTH = 32
35 private val CODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray()
36
37 /**
38 * 認可エンドポイントのルーティングを設定
39 * GET /authorize: 認可画面を表示
40 * POST /authorize: 認可リクエストを処理
23 object EndSessionRouter : KoinComponent {
24 private val plugin: MineAuth by inject()
25
26 /**
27 * GET /oauth2/end_session エンドポイントを登録する
28 *
29 * RP-Initiated Logoutに基づくエンドセッションエンドポイント
24 object IntrospectRouter : KoinComponent {
25 private val plugin: MineAuth by inject()
26
27 /**
28 * POST /oauth2/introspect エンドポイントを登録する
29 *
30 * RFC 7662に基づくトークンイントロスペクションエンドポイント
13 import party.morino.mineauth.core.web.router.auth.oauth.TokenRouter.tokenRouter
14
15 object OAuthRouter: KoinComponent {
16 fun Route.oauthRouter() {
17 route("/oauth2") {
18 authorizeRouter()
19 tokenRouter()
19 object ProfileRouter : KoinComponent {
20 private val config: MineAuthConfig by inject()
21
22 fun Route.profileRouter() {
23 authenticate("user-oauth-token") {
24 // OIDC UserInfo Endpoint
25 // アクセストークンのスコープに基づいてクレームを返す
29 object RevokeRouter : KoinComponent {
30 private val plugin: MineAuth by inject()
31
32 /**
33 * POST /oauth2/revoke エンドポイントを登録する
34 *
35 * RFC 7009に基づくトークン失効エンドポイント
32 // RFC 6749 Section 4.1.2: 認可コードの最大有効期間(10分推奨)
33 private const val AUTHORIZATION_CODE_LIFETIME_MS = 10 * 60 * 1000L
34
35 fun Route.tokenRouter() {
36 post("/token") {
37 val formParameters = call.receiveParameters()
38 val grantType = formParameters["grant_type"]
6
7
8 object CommonRouter {
9 fun Route.commonRouter() {
10 route("/server") {
11 serverRouter()
12 }
5 import party.morino.mineauth.core.web.router.common.server.PluginsRouter.pluginsRoutes
6
7 object ServerRouter {
8 fun Route.serverRouter() {
9 playersRouter()
10 pluginsRoutes()
11 }
3 import io.ktor.server.routing.*
4
5 object UserRouter {
6 fun Route.userRouter() {
7
8 }
9 }
6 import party.morino.mineauth.api.model.common.ProfileData
7
8 object PlayersRouter {
9 fun Route.playersRouter() {
10 get("/players") {
11 val players = Bukkit.getOnlinePlayers().map { ProfileData(it.name, it.uniqueId) }
12 call.respond(players)
5 import party.morino.mineauth.core.integration.IntegrationInitializer
6
7 object PluginRouter {
8 fun Route.pluginRouter() {
9 get {
10 call.respondText("Hello, plugin!")
11 }
28 class ServiceAccountCommand : KoinComponent {
29
30 // サービスアカウントトークンの有効期間(1年)
31 private val TOKEN_LIFETIME_MS = 365L * 24 * 3600 * 1000
32
33 /**
34 * サービスアカウント作成ダイアログを表示するコマンド
27
28 return try {
29 // 通常のメソッド呼び出し
30 val result = javaMethod.invoke(metadata.handlerInstance, *resolvedParams.toTypedArray())
31 result.right()
32 } catch (e: HttpError) {
33 // HttpErrorは専用のエラー型に変換する
88
89 // suspend関数はContinuationを最後の引数として受け取る
90 val args = (params + proxyContinuation).toTypedArray()
91 val result = javaMethod.invoke(instance, *args)
92
93 // COROUTINE_SUSPENDED の場合はそのまま返す(コルーチンがサスペンド中)
94 if (isCoroutineSuspended(result)) COROUTINE_SUSPENDED else result
4 * HTTPステータスコードを表す列挙型
5 */
6 enum class HttpStatus(val code: Int) {
7 OK(200),
8 CREATED(201),
9 BAD_REQUEST(400),
10 UNAUTHORIZED(401),
5 */
6 enum class HttpStatus(val code: Int) {
7 OK(200),
8 CREATED(201),
9 BAD_REQUEST(400),
10 UNAUTHORIZED(401),
11 FORBIDDEN(403),
6 enum class HttpStatus(val code: Int) {
7 OK(200),
8 CREATED(201),
9 BAD_REQUEST(400),
10 UNAUTHORIZED(401),
11 FORBIDDEN(403),
12 NOT_FOUND(404),
7 OK(200),
8 CREATED(201),
9 BAD_REQUEST(400),
10 UNAUTHORIZED(401),
11 FORBIDDEN(403),
12 NOT_FOUND(404),
13 INTERNAL_SERVER_ERROR(500)
8 CREATED(201),
9 BAD_REQUEST(400),
10 UNAUTHORIZED(401),
11 FORBIDDEN(403),
12 NOT_FOUND(404),
13 INTERNAL_SERVER_ERROR(500)
14 // 必要に応じて他のステータスコードを追加してください
9 BAD_REQUEST(400),
10 UNAUTHORIZED(401),
11 FORBIDDEN(403),
12 NOT_FOUND(404),
13 INTERNAL_SERVER_ERROR(500)
14 // 必要に応じて他のステータスコードを追加してください
15 }
10 UNAUTHORIZED(401),
11 FORBIDDEN(403),
12 NOT_FOUND(404),
13 INTERNAL_SERVER_ERROR(500)
14 // 必要に応じて他のステータスコードを追加してください
15 }
30 sender.sendMessage("You are already registered if you want to change your password use /moripaapi change")
31 return
32 }
33 val password = RandomStringUtils.randomAlphanumeric(20)
34 sender.sendRichMessage(
35 "Password is $password <yellow><click:copy_to_clipboard:'$password'>Click to copy</click></yellow>"
36 )
58 sender.sendMessage("You are not already registered if you want to register use /moripaapi register")
59 return
60 }
61 val password = RandomStringUtils.randomAlphanumeric(20)
62 sender.sendRichMessage(
63 "Password is $password. <yellow><click:copy_to_clipboard:'$password'>Click to copy</click></yellow>"
64 )
28 class ServiceAccountCommand : KoinComponent {
29
30 // サービスアカウントトークンの有効期間(1年)
31 private val TOKEN_LIFETIME_MS = 365L * 24 * 3600 * 1000
32
33 /**
34 * サービスアカウント作成ダイアログを表示するコマンド
28 class ServiceAccountCommand : KoinComponent {
29
30 // サービスアカウントトークンの有効期間(1年)
31 private val TOKEN_LIFETIME_MS = 365L * 24 * 3600 * 1000
32
33 /**
34 * サービスアカウント作成ダイアログを表示するコマンド
28 class ServiceAccountCommand : KoinComponent {
29
30 // サービスアカウントトークンの有効期間(1年)
31 private val TOKEN_LIFETIME_MS = 365L * 24 * 3600 * 1000
32
33 /**
34 * サービスアカウント作成ダイアログを表示するコマンド
28 class ServiceAccountCommand : KoinComponent {
29
30 // サービスアカウントトークンの有効期間(1年)
31 private val TOKEN_LIFETIME_MS = 365L * 24 * 3600 * 1000
32
33 /**
34 * サービスアカウント作成ダイアログを表示するコマンド
225 )
226 sender.sendRichMessage(
227 "<gray>トークン:</gray> <yellow><click:copy_to_clipboard:'$token'>" +
228 "${token.take(20)}...</click></yellow> <dark_gray>(クリックでコピー)</dark_gray>"
229 )
230 }
231 }
10 */
11 object Accounts : Table("accounts") {
12 // UUIDv7形式のアカウントID(時間ソート可能)
13 val accountId = varchar("account_id", 36)
14
15 // アカウント種別: "player" または "service"
16 val accountType = varchar("account_type", 20)
13 val accountId = varchar("account_id", 36)
14
15 // アカウント種別: "player" または "service"
16 val accountType = varchar("account_type", 20)
17
18 // 識別子: プレイヤーの場合はMinecraft UUID、サービスの場合はサービス名
19 val identifier = varchar("identifier", 64)
16 val accountType = varchar("account_type", 20)
17
18 // 識別子: プレイヤーの場合はMinecraft UUID、サービスの場合はサービス名
19 val identifier = varchar("identifier", 64)
20
21 // 作成日時
22 val createdAt = datetime("created_at").clientDefault { LocalDateTime.now() }
67 password = config.password
68
69 // コネクションプール設定
70 maximumPoolSize = 10
71 minimumIdle = 2
72 idleTimeout = 300000 // 5分
73 connectionTimeout = 10000 // 10秒
69 // コネクションプール設定
70 maximumPoolSize = 10
71 minimumIdle = 2
72 idleTimeout = 300000 // 5分
73 connectionTimeout = 10000 // 10秒
74 maxLifetime = 1800000 // 30分
75 }
70 maximumPoolSize = 10
71 minimumIdle = 2
72 idleTimeout = 300000 // 5分
73 connectionTimeout = 10000 // 10秒
74 maxLifetime = 1800000 // 30分
75 }
76
71 minimumIdle = 2
72 idleTimeout = 300000 // 5分
73 connectionTimeout = 10000 // 10秒
74 maxLifetime = 1800000 // 30分
75 }
76
77 dataSource = HikariDataSource(hikariConfig)
11 */
12 object OAuthClients : Table("oauth_clients") {
13 // UUIDv7形式のクライアントID(時間ソート可能)
14 val clientId = varchar("client_id", 36)
15
16 // クライアント名(表示用)
17 val clientName = varchar("client_name", 255)
14 val clientId = varchar("client_id", 36)
15
16 // クライアント名(表示用)
17 val clientName = varchar("client_name", 255)
18
19 // クライアント種別: "public" または "confidential"
20 val clientType = varchar("client_type", 20)
17 val clientName = varchar("client_name", 255)
18
19 // クライアント種別: "public" または "confidential"
20 val clientType = varchar("client_type", 20)
21
22 // クライアントシークレットのArgon2idハッシュ(Publicクライアントの場合はNULL)
23 val clientSecretHash = varchar("client_secret_hash", 255).nullable()
20 val clientType = varchar("client_type", 20)
21
22 // クライアントシークレットのArgon2idハッシュ(Publicクライアントの場合はNULL)
23 val clientSecretHash = varchar("client_secret_hash", 255).nullable()
24
25 // リダイレクトURI(正規表現パターンをサポート)
26 val redirectUri = varchar("redirect_uri", 2048)
23 val clientSecretHash = varchar("client_secret_hash", 255).nullable()
24
25 // リダイレクトURI(正規表現パターンをサポート)
26 val redirectUri = varchar("redirect_uri", 2048)
27
28 // 発行者アカウントID(Accountsテーブルへの外部キー)
29 val issuerAccountId = varchar("issuer_account_id", 36)
26 val redirectUri = varchar("redirect_uri", 2048)
27
28 // 発行者アカウントID(Accountsテーブルへの外部キー)
29 val issuerAccountId = varchar("issuer_account_id", 36)
30 .references(Accounts.accountId, onDelete = ReferenceOption.CASCADE)
31
32 // 作成日時
12 */
13 object RevokedTokens : Table("revoked_tokens") {
14 // トークンのJWT ID(jti claim)- UUIDv4形式
15 val tokenId = varchar("token_id", 36)
16
17 // トークン種別: "access_token" または "refresh_token"
18 val tokenType = varchar("token_type", 20)
15 val tokenId = varchar("token_id", 36)
16
17 // トークン種別: "access_token" または "refresh_token"
18 val tokenType = varchar("token_type", 20)
19
20 // 失効対象のクライアントID
21 val clientId = varchar("client_id", 36)
18 val tokenType = varchar("token_type", 20)
19
20 // 失効対象のクライアントID
21 val clientId = varchar("client_id", 36)
22
23 // 失効日時
24 val revokedAt = datetime("revoked_at").clientDefault { LocalDateTime.now() }
10 */
11 object ServiceAccountTokens : Table("service_account_tokens") {
12 // トークンのJWT ID(jti claim)
13 val tokenId = varchar("token_id", 36)
14
15 // アカウントID(Accounts.accountIdへの参照)
16 val accountId = varchar("account_id", 36).references(Accounts.accountId)
13 val tokenId = varchar("token_id", 36)
14
15 // アカウントID(Accounts.accountIdへの参照)
16 val accountId = varchar("account_id", 36).references(Accounts.accountId)
17
18 // トークンハッシュ(SHA-256、トークン漏洩時の検証用)
19 val tokenHash = varchar("token_hash", 64)
16 val accountId = varchar("account_id", 36).references(Accounts.accountId)
17
18 // トークンハッシュ(SHA-256、トークン漏洩時の検証用)
19 val tokenHash = varchar("token_hash", 64)
20
21 // 作成者のプレイヤーUUID
22 val createdBy = varchar("created_by", 36)
19 val tokenHash = varchar("token_hash", 64)
20
21 // 作成者のプレイヤーUUID
22 val createdBy = varchar("created_by", 36)
23
24 // 作成日時
25 val createdAt = datetime("created_at").clientDefault { LocalDateTime.now() }
3 import org.jetbrains.exposed.v1.core.Table
4
5 object UserAuthData: Table() {
6 val uuid = varchar("uuid", 36)
7 val password = varchar("password", 255)
8 override val primaryKey = PrimaryKey(uuid)
9 }
4
5 object UserAuthData: Table() {
6 val uuid = varchar("uuid", 36)
7 val password = varchar("password", 255)
8 override val primaryKey = PrimaryKey(uuid)
9 }
83 if (clientName.isBlank()) {
84 return Either.Left("クライアント名を入力してください")
85 }
86 if (clientName.length > 255) {
87 return Either.Left("クライアント名は255文字以内で入力してください")
88 }
89
57 if (serviceName.isBlank()) {
58 return Either.Left("サービス名を入力してください")
59 }
60 if (serviceName.length > 64) {
61 return Either.Left("サービス名は64文字以内で入力してください")
62 }
63 if (!SERVICE_NAME_PATTERN.matches(serviceName)) {
15 plugin.logger.info("${configFile.name} not found. Creating new one.")
16 configFile.parentFile.mkdirs()
17 configFile.createNewFile()
18 val configData = WebServerConfigData(8080, null)
19 configFile.writeText(json.encodeToString(configData))
20 }
21 val configData: WebServerConfigData = json.decodeFromString(configFile.readText())
64 privateKeyFile.createNewFile()
65 publicKeyFile.createNewFile()
66 val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
67 keyPairGenerator.initialize(2048, SecureRandom())
68 val keyPair = keyPairGenerator.generateKeyPair()
69
70 JcaPEMWriter(privateKeyFile.writer()).use { pemWriter ->
87 }
88
89 val startDate = Date()
90 val endDate = Date(System.currentTimeMillis() + 365L * 24L * 60L * 60L * 1000L) // 1 year validity
91 val serialNumber = BigInteger.valueOf(System.currentTimeMillis())
92 val subjectDN = X500Name("CN=Test Certificate")
93 val issuerDN = subjectDN // self-signed
87 }
88
89 val startDate = Date()
90 val endDate = Date(System.currentTimeMillis() + 365L * 24L * 60L * 60L * 1000L) // 1 year validity
91 val serialNumber = BigInteger.valueOf(System.currentTimeMillis())
92 val subjectDN = X500Name("CN=Test Certificate")
93 val issuerDN = subjectDN // self-signed
87 }
88
89 val startDate = Date()
90 val endDate = Date(System.currentTimeMillis() + 365L * 24L * 60L * 60L * 1000L) // 1 year validity
91 val serialNumber = BigInteger.valueOf(System.currentTimeMillis())
92 val subjectDN = X500Name("CN=Test Certificate")
93 val issuerDN = subjectDN // self-signed
87 }
88
89 val startDate = Date()
90 val endDate = Date(System.currentTimeMillis() + 365L * 24L * 60L * 60L * 1000L) // 1 year validity
91 val serialNumber = BigInteger.valueOf(System.currentTimeMillis())
92 val subjectDN = X500Name("CN=Test Certificate")
93 val issuerDN = subjectDN // self-signed
87 }
88
89 val startDate = Date()
90 val endDate = Date(System.currentTimeMillis() + 365L * 24L * 60L * 60L * 1000L) // 1 year validity
91 val serialNumber = BigInteger.valueOf(System.currentTimeMillis())
92 val subjectDN = X500Name("CN=Test Certificate")
93 val issuerDN = subjectDN // self-signed
131 val certificateFile = generatedDir.resolve("certificate.pem")
132 val (privateKey, _) = getKeys()
133
134 val randomPassword = RandomStringUtils.randomAlphabetic(16)
135 val keyStore = KeyStore.getInstance("JKS")
136 keyStore.load(null, null)
137 keyStore.setKeyEntry(
298 .replace("\r", "\\r")
299 .replace("\t", "\\t")
300 .replace(Regex("[\\x00-\\x1F\\x7F]"), "")
301 .take(500) // 長すぎる入力を切り詰める
302 }
303 }
304 }
12 // OWASP推奨パラメータ(最小要件)
13 // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
14 private val argon2Function = Argon2Function.getInstance(
15 19456, // メモリ: 19MB(OWASP最小推奨)
16 2, // イテレーション: 2
17 1, // 並列度: 1
18 32, // ハッシュ長: 32バイト
15 19456, // メモリ: 19MB(OWASP最小推奨)
16 2, // イテレーション: 2
17 1, // 並列度: 1
18 32, // ハッシュ長: 32バイト
19 Argon2.ID // Argon2id(サイドチャネル攻撃とGPU攻撃の両方に耐性)
20 )
21
142 setProperty("resource.loader.file.class", FileResourceLoader::class.java.name)
143 setProperty("resource.loader.file.path", plugin.dataFolder.resolve("templates").absolutePath)
144 }
145 val jwkProvider = JwkProviderBuilder(jwtConfigData.issuer).cached(10, 24, TimeUnit.HOURS).rateLimited(10, 1, TimeUnit.MINUTES).build()
146 install(Authentication) {
147 jwt(JwtCompleteCode.USER_TOKEN.code) {
148 realm = jwtConfigData.realm
142 setProperty("resource.loader.file.class", FileResourceLoader::class.java.name)
143 setProperty("resource.loader.file.path", plugin.dataFolder.resolve("templates").absolutePath)
144 }
145 val jwkProvider = JwkProviderBuilder(jwtConfigData.issuer).cached(10, 24, TimeUnit.HOURS).rateLimited(10, 1, TimeUnit.MINUTES).build()
146 install(Authentication) {
147 jwt(JwtCompleteCode.USER_TOKEN.code) {
148 realm = jwtConfigData.realm
142 setProperty("resource.loader.file.class", FileResourceLoader::class.java.name)
143 setProperty("resource.loader.file.path", plugin.dataFolder.resolve("templates").absolutePath)
144 }
145 val jwkProvider = JwkProviderBuilder(jwtConfigData.issuer).cached(10, 24, TimeUnit.HOURS).rateLimited(10, 1, TimeUnit.MINUTES).build()
146 install(Authentication) {
147 jwt(JwtCompleteCode.USER_TOKEN.code) {
148 realm = jwtConfigData.realm
147 jwt(JwtCompleteCode.USER_TOKEN.code) {
148 realm = jwtConfigData.realm
149 verifier(jwkProvider, jwtConfigData.issuer) {
150 acceptLeeway(3)
151 }
152
153 validate { credential ->
175 jwt(JwtCompleteCode.SERVICE_TOKEN.code) {
176 realm = jwtConfigData.realm
177 verifier(jwkProvider, jwtConfigData.issuer) {
178 acceptLeeway(3)
179 }
180
181 validate { credential ->
112 clientId = jwt.getClaim("client_id").asString(),
113 username = username,
114 tokenType = responseTokenType,
115 exp = jwt.expiresAt?.time?.div(1000),
116 iat = jwt.issuedAt?.time?.div(1000),
117 nbf = jwt.notBefore?.time?.div(1000),
118 sub = playerUniqueId,
113 username = username,
114 tokenType = responseTokenType,
115 exp = jwt.expiresAt?.time?.div(1000),
116 iat = jwt.issuedAt?.time?.div(1000),
117 nbf = jwt.notBefore?.time?.div(1000),
118 sub = playerUniqueId,
119 aud = jwt.audience?.firstOrNull(),
114 tokenType = responseTokenType,
115 exp = jwt.expiresAt?.time?.div(1000),
116 iat = jwt.issuedAt?.time?.div(1000),
117 nbf = jwt.notBefore?.time?.div(1000),
118 sub = playerUniqueId,
119 aud = jwt.audience?.firstOrNull(),
120 iss = jwt.issuer,
136 if (!authHeader.startsWith("Basic ", ignoreCase = true)) return null
137 return try {
138 // Base64デコードして "client_id:client_secret" 形式を分割
139 val decoded = String(Base64.getDecoder().decode(authHeader.substring(6)), Charsets.UTF_8)
140 val colonIndex = decoded.indexOf(':')
141 if (colonIndex < 0) return null
142 val id = decoded.substring(0, colonIndex)
160 // トークンの有効期限を取得
161 val expiresAt = jwt.expiresAt?.let {
162 LocalDateTime.ofInstant(Instant.ofEpochMilli(it.time), ZoneId.systemDefault())
163 } ?: LocalDateTime.now().plusDays(30)
164
165 // 失効トークンとして登録
166 val result = RevokedTokenRepository.revoke(tokenId, tokenType, clientId, expiresAt)
210 ): String = JWT.create().withIssuer(get<JWTConfigData>().issuer)
211 .withAudience(data.clientId)
212 .withNotBefore(Date(System.currentTimeMillis()))
213 .withExpiresAt(Date(System.currentTimeMillis() + (3_600_000.toLong() * 24 * 30)))
214 .withIssuedAt(Date(System.currentTimeMillis())).withJWTId(UUID.randomUUID().toString())
215 .withClaim("client_id", clientId)
216 .withClaim("playerUniqueId", data.uniqueId.toString())
210 ): String = JWT.create().withIssuer(get<JWTConfigData>().issuer)
211 .withAudience(data.clientId)
212 .withNotBefore(Date(System.currentTimeMillis()))
213 .withExpiresAt(Date(System.currentTimeMillis() + (3_600_000.toLong() * 24 * 30)))
214 .withIssuedAt(Date(System.currentTimeMillis())).withJWTId(UUID.randomUUID().toString())
215 .withClaim("client_id", clientId)
216 .withClaim("playerUniqueId", data.uniqueId.toString())
317 private fun calculateAtHash(accessToken: String): String {
318 val digest = MessageDigest.getInstance("SHA-256")
319 val hash = digest.digest(accessToken.toByteArray(Charsets.US_ASCII))
320 val leftHalf = hash.copyOf(16) // 左128ビット
321 return Base64.getUrlEncoder().withoutPadding().encodeToString(leftHalf)
322 }
323
372 .withIssuer(configData.issuer) // iss: Issuer Identifier
373 .withSubject(sub) // sub: Subject Identifier
374 .withAudience(clientId ?: data.clientId) // aud: Audience
375 .withExpiresAt(Date(now.time + 3600 * 1000)) // exp: 1時間後
376 .withIssuedAt(now) // iat: 発行時刻
377 // 条件付きクレーム
378 .withClaim("auth_time", data.authTime / 1000) // auth_time: 認証時刻(秒単位)
372 .withIssuer(configData.issuer) // iss: Issuer Identifier
373 .withSubject(sub) // sub: Subject Identifier
374 .withAudience(clientId ?: data.clientId) // aud: Audience
375 .withExpiresAt(Date(now.time + 3600 * 1000)) // exp: 1時間後
376 .withIssuedAt(now) // iat: 発行時刻
377 // 条件付きクレーム
378 .withClaim("auth_time", data.authTime / 1000) // auth_time: 認証時刻(秒単位)
375 .withExpiresAt(Date(now.time + 3600 * 1000)) // exp: 1時間後
376 .withIssuedAt(now) // iat: 発行時刻
377 // 条件付きクレーム
378 .withClaim("auth_time", data.authTime / 1000) // auth_time: 認証時刻(秒単位)
379 .apply {
380 // nonceが存在する場合のみ含める(リプレイ攻撃防止用)
381 data.nonce?.let { withClaim("nonce", it) }
155 // gRPCエクスポーターを構築
156 val builder = OtlpGrpcSpanExporter.builder()
157 .setEndpoint(config.endpoint)
158 .setTimeout(java.time.Duration.ofSeconds(30)) // タイムアウトを30秒に設定
159
160 // ヘッダーを設定(認証用など)
161 config.headers.forEach { (key, value) ->
174
175 val builder = OtlpHttpSpanExporter.builder()
176 .setEndpoint(normalizedEndpoint)
177 .setTimeout(java.time.Duration.ofSeconds(30)) // タイムアウトを30秒に設定
178
179 // ヘッダーを設定(認証用など)
180 config.headers.forEach { (key, value) ->
33 fun fromItemMeta(itemMeta: ItemMeta): ItemMetaData {
34 return ItemMetaData(
35 enchantment = itemMeta.enchants.map { it.key.key.key to it.value }.toMap(),
36 displayName = ComponentData.Companion.fromComponent(itemMeta.displayName() ?: ComponentData("").toComponent()),
37 customModelData = if (itemMeta.hasCustomModelData()) itemMeta.customModelData else null,
38 )
39 }
25 type = MaterialData.fromMaterial(itemStack.type),
26 amount = itemStack.amount,
27 lore = itemStack.lore()?.map { ComponentData.Companion.fromComponent(it) } ?: emptyList(),
28 meta = ItemMetaData.fromItemMeta(itemStack.itemMeta ?: Bukkit.getItemFactory().getItemMeta(itemStack.type))
29 )
30 }
31 }
15 ){
16 fun toLocation() = Location(Bukkit.getWorld(world), x, y, z, yaw, pitch)
17 companion object {
18 fun fromLocation(location: Location) = LocationData(location.world.name, location.x, location.y, location.z, location.yaw, location.pitch)
19 }
20 }
46 .body(
47 listOf(
48 DialogBody.plainMessage(
49 MiniMessage.miniMessage().deserialize("新しいOAuthクライアントを作成します。 <newline>必要な情報を入力してください。")
50 )
51 )
52 )
179 operationId = "get_oauth2_authorize",
180 tags = listOf(TAG_OAUTH),
181 parameters = listOf(
182 Parameter(name = "response_type", location = "query", required = true, schema = Schema(type = "string"), description = "Must be 'code'"),
183 Parameter(name = "client_id", location = "query", required = true, schema = Schema(type = "string"), description = "Client identifier"),
184 Parameter(name = "redirect_uri", location = "query", required = true, schema = Schema(type = "string"), description = "Redirect URI"),
185 Parameter(name = "scope", location = "query", required = true, schema = Schema(type = "string"), description = "Requested scopes (space-separated)"),
180 tags = listOf(TAG_OAUTH),
181 parameters = listOf(
182 Parameter(name = "response_type", location = "query", required = true, schema = Schema(type = "string"), description = "Must be 'code'"),
183 Parameter(name = "client_id", location = "query", required = true, schema = Schema(type = "string"), description = "Client identifier"),
184 Parameter(name = "redirect_uri", location = "query", required = true, schema = Schema(type = "string"), description = "Redirect URI"),
185 Parameter(name = "scope", location = "query", required = true, schema = Schema(type = "string"), description = "Requested scopes (space-separated)"),
186 Parameter(name = "state", location = "query", required = true, schema = Schema(type = "string"), description = "CSRF protection state"),
181 parameters = listOf(
182 Parameter(name = "response_type", location = "query", required = true, schema = Schema(type = "string"), description = "Must be 'code'"),
183 Parameter(name = "client_id", location = "query", required = true, schema = Schema(type = "string"), description = "Client identifier"),
184 Parameter(name = "redirect_uri", location = "query", required = true, schema = Schema(type = "string"), description = "Redirect URI"),
185 Parameter(name = "scope", location = "query", required = true, schema = Schema(type = "string"), description = "Requested scopes (space-separated)"),
186 Parameter(name = "state", location = "query", required = true, schema = Schema(type = "string"), description = "CSRF protection state"),
187 Parameter(name = "code_challenge", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE code challenge"),
182 Parameter(name = "response_type", location = "query", required = true, schema = Schema(type = "string"), description = "Must be 'code'"),
183 Parameter(name = "client_id", location = "query", required = true, schema = Schema(type = "string"), description = "Client identifier"),
184 Parameter(name = "redirect_uri", location = "query", required = true, schema = Schema(type = "string"), description = "Redirect URI"),
185 Parameter(name = "scope", location = "query", required = true, schema = Schema(type = "string"), description = "Requested scopes (space-separated)"),
186 Parameter(name = "state", location = "query", required = true, schema = Schema(type = "string"), description = "CSRF protection state"),
187 Parameter(name = "code_challenge", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE code challenge"),
188 Parameter(name = "code_challenge_method", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE method (S256)"),
183 Parameter(name = "client_id", location = "query", required = true, schema = Schema(type = "string"), description = "Client identifier"),
184 Parameter(name = "redirect_uri", location = "query", required = true, schema = Schema(type = "string"), description = "Redirect URI"),
185 Parameter(name = "scope", location = "query", required = true, schema = Schema(type = "string"), description = "Requested scopes (space-separated)"),
186 Parameter(name = "state", location = "query", required = true, schema = Schema(type = "string"), description = "CSRF protection state"),
187 Parameter(name = "code_challenge", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE code challenge"),
188 Parameter(name = "code_challenge_method", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE method (S256)"),
189 Parameter(name = "nonce", location = "query", required = false, schema = Schema(type = "string"), description = "OIDC nonce for replay protection"),
184 Parameter(name = "redirect_uri", location = "query", required = true, schema = Schema(type = "string"), description = "Redirect URI"),
185 Parameter(name = "scope", location = "query", required = true, schema = Schema(type = "string"), description = "Requested scopes (space-separated)"),
186 Parameter(name = "state", location = "query", required = true, schema = Schema(type = "string"), description = "CSRF protection state"),
187 Parameter(name = "code_challenge", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE code challenge"),
188 Parameter(name = "code_challenge_method", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE method (S256)"),
189 Parameter(name = "nonce", location = "query", required = false, schema = Schema(type = "string"), description = "OIDC nonce for replay protection"),
190 ),
185 Parameter(name = "scope", location = "query", required = true, schema = Schema(type = "string"), description = "Requested scopes (space-separated)"),
186 Parameter(name = "state", location = "query", required = true, schema = Schema(type = "string"), description = "CSRF protection state"),
187 Parameter(name = "code_challenge", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE code challenge"),
188 Parameter(name = "code_challenge_method", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE method (S256)"),
189 Parameter(name = "nonce", location = "query", required = false, schema = Schema(type = "string"), description = "OIDC nonce for replay protection"),
190 ),
191 responses = mapOf(
186 Parameter(name = "state", location = "query", required = true, schema = Schema(type = "string"), description = "CSRF protection state"),
187 Parameter(name = "code_challenge", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE code challenge"),
188 Parameter(name = "code_challenge_method", location = "query", required = false, schema = Schema(type = "string"), description = "PKCE method (S256)"),
189 Parameter(name = "nonce", location = "query", required = false, schema = Schema(type = "string"), description = "OIDC nonce for replay protection"),
190 ),
191 responses = mapOf(
192 "200" to Response(description = "Authorization screen (HTML)"),
195 ),
196 post = Operation(
197 summary = "Process authorization",
198 description = "Processes the authorization request with user credentials. Returns authorization code via redirect.",
199 operationId = "post_oauth2_authorize",
200 tags = listOf(TAG_OAUTH),
201 requestBody = RequestBody(
214 "code_challenge" to Schema(type = "string"),
215 "code_challenge_method" to Schema(type = "string"),
216 ),
217 required = listOf("username", "password", "response_type", "client_id", "redirect_uri", "scope", "state", "code_challenge"),
218 )
219 )
220 ),
228 "/oauth2/token" to PathItem(
229 post = Operation(
230 summary = "Token endpoint",
231 description = "Issues access tokens. Supports authorization_code and refresh_token grant types. RFC 6749 Section 4.1.3.",
232 operationId = "post_oauth2_token",
233 tags = listOf(TAG_OAUTH),
234 requestBody = RequestBody(
237 schema = Schema(
238 type = "object",
239 properties = mapOf(
240 "grant_type" to Schema(type = "string", description = "authorization_code or refresh_token"),
241 "code" to Schema(type = "string", description = "Authorization code (for authorization_code grant)"),
242 "redirect_uri" to Schema(type = "string"),
243 "client_id" to Schema(type = "string"),
238 type = "object",
239 properties = mapOf(
240 "grant_type" to Schema(type = "string", description = "authorization_code or refresh_token"),
241 "code" to Schema(type = "string", description = "Authorization code (for authorization_code grant)"),
242 "redirect_uri" to Schema(type = "string"),
243 "client_id" to Schema(type = "string"),
244 "client_secret" to Schema(type = "string", description = "Required for confidential clients"),
241 "code" to Schema(type = "string", description = "Authorization code (for authorization_code grant)"),
242 "redirect_uri" to Schema(type = "string"),
243 "client_id" to Schema(type = "string"),
244 "client_secret" to Schema(type = "string", description = "Required for confidential clients"),
245 "code_verifier" to Schema(type = "string", description = "PKCE code verifier"),
246 "refresh_token" to Schema(type = "string", description = "Refresh token (for refresh_token grant)"),
247 ),
243 "client_id" to Schema(type = "string"),
244 "client_secret" to Schema(type = "string", description = "Required for confidential clients"),
245 "code_verifier" to Schema(type = "string", description = "PKCE code verifier"),
246 "refresh_token" to Schema(type = "string", description = "Refresh token (for refresh_token grant)"),
247 ),
248 required = listOf("grant_type", "client_id"),
249 )
277 type = "object",
278 properties = mapOf(
279 "token" to Schema(type = "string", description = "Token to revoke"),
280 "token_type_hint" to Schema(type = "string", description = "access_token or refresh_token"),
281 "client_id" to Schema(type = "string"),
282 "client_secret" to Schema(type = "string"),
283 ),
314 properties = mapOf(
315 "sub" to Schema(type = "string", description = "Player UUID"),
316 "name" to Schema(type = "string", description = "Player name"),
317 "preferred_username" to Schema(type = "string", description = "Player name"),
318 "email" to Schema(type = "string", description = "Generated email address"),
319 "roles" to Schema(type = "array", items = Schema(type = "string"), description = "LuckPerms groups"),
320 ),
316 "name" to Schema(type = "string", description = "Player name"),
317 "preferred_username" to Schema(type = "string", description = "Player name"),
318 "email" to Schema(type = "string", description = "Generated email address"),
319 "roles" to Schema(type = "array", items = Schema(type = "string"), description = "LuckPerms groups"),
320 ),
321 required = listOf("sub"),
322 )
62 }
63 if (webServerConfigData.ssl != null) {
64 val keyStoreFile = plugin.dataFolder.resolve("keystore.jks")
65 val keystore = KeyStore.getInstance(keyStoreFile, webServerConfigData.ssl.keyStorePassword.toCharArray())
66 sslConnector(keyStore = keystore, keyAlias = webServerConfigData.ssl.keyAlias, keyStorePassword = { webServerConfigData.ssl.keyStorePassword.toCharArray() }, privateKeyPassword = { webServerConfigData.ssl.privateKeyPassword.toCharArray() }) {
67 port = webServerConfigData.ssl.sslPort
68 keyStorePath = keyStoreFile
63 if (webServerConfigData.ssl != null) {
64 val keyStoreFile = plugin.dataFolder.resolve("keystore.jks")
65 val keystore = KeyStore.getInstance(keyStoreFile, webServerConfigData.ssl.keyStorePassword.toCharArray())
66 sslConnector(keyStore = keystore, keyAlias = webServerConfigData.ssl.keyAlias, keyStorePassword = { webServerConfigData.ssl.keyStorePassword.toCharArray() }, privateKeyPassword = { webServerConfigData.ssl.privateKeyPassword.toCharArray() }) {
67 port = webServerConfigData.ssl.sslPort
68 keyStorePath = keyStoreFile
69 }
142 setProperty("resource.loader.file.class", FileResourceLoader::class.java.name)
143 setProperty("resource.loader.file.path", plugin.dataFolder.resolve("templates").absolutePath)
144 }
145 val jwkProvider = JwkProviderBuilder(jwtConfigData.issuer).cached(10, 24, TimeUnit.HOURS).rateLimited(10, 1, TimeUnit.MINUTES).build()
146 install(Authentication) {
147 jwt(JwtCompleteCode.USER_TOKEN.code) {
148 realm = jwtConfigData.realm
70 }
71
72 if (!OAuthService.validateClientAndRedirectUri(clientData, redirectUri)) {
73 plugin.logger.warning("Authorize error: Invalid redirect_uri - client_id=$clientId, redirect_uri=$redirectUri")
74 call.respond(HttpStatusCode.BadRequest, "Invalid redirect_uri")
75 return@get
76 }
79 if (scope == null || responseType != "code" || state == null) {
80 plugin.logger.warning("Authorize error: Invalid request - missing required parameters")
81 val errorState = state ?: ""
82 val errorUri = buildErrorRedirectUri(redirectUri, "invalid_request", "Missing required parameters", errorState)
83 call.respondRedirect(errorUri)
84 return@get
85 }
88 if (!validateScope(scope)) {
89 val invalidScopes = OAuthScope.findInvalidScopes(scope)
90 plugin.logger.warning("Authorize error: Invalid scope(s): $invalidScopes - client_id=$clientId")
91 val errorUri = buildErrorRedirectUri(redirectUri, "invalid_scope", "Invalid scope(s): ${invalidScopes.joinToString(", ")}", state)
92 call.respondRedirect(errorUri)
93 return@get
94 }
96 // PKCE(Proof Key for Code Exchange)のバリデーション
97 if (!validatePKCE(codeChallenge, codeChallengeMethod)) {
98 plugin.logger.warning("Authorize error: Unsupported PKCE method - client_id=$clientId")
99 val errorUri = buildErrorRedirectUri(redirectUri, "invalid_request", "This server only supports S256 code_challenge_method", state)
100 call.respondRedirect(errorUri)
101 return@get
102 }
144 val nonce = formParameters["nonce"]
145
146 // パラメータのバリデーション
147 if (username == null || password == null || responseType != "code" || clientId == null || redirectUri == null || scope == null || state == null || codeChallenge == null) {
148 // redirect_uriとstateが存在する場合のみエラーリダイレクト
149 if (redirectUri != null && state != null) {
150 val errorUri = buildErrorRedirectUri(redirectUri, "invalid_request", "It does not have the required parameters", state)
147 if (username == null || password == null || responseType != "code" || clientId == null || redirectUri == null || scope == null || state == null || codeChallenge == null) {
148 // redirect_uriとstateが存在する場合のみエラーリダイレクト
149 if (redirectUri != null && state != null) {
150 val errorUri = buildErrorRedirectUri(redirectUri, "invalid_request", "It does not have the required parameters", state)
151 call.respondRedirect(errorUri)
152 } else {
153 call.respond(HttpStatusCode.BadRequest, "Invalid request: missing required parameters")
164 return@post
165 }
166 if (!OAuthService.validateClientAndRedirectUri(clientData, redirectUri)) {
167 plugin.logger.warning("Authorize POST error: Invalid redirect_uri - client_id=$clientId, redirect_uri=$redirectUri")
168 call.respond(HttpStatusCode.BadRequest, "Invalid redirect_uri")
169 return@post
170 }
173 if (!validateScope(scope)) {
174 val invalidScopes = OAuthScope.findInvalidScopes(scope)
175 plugin.logger.warning("Authorize POST error: Invalid scope(s): $invalidScopes - client_id=$clientId")
176 val errorUri = buildErrorRedirectUri(redirectUri, "invalid_scope", "Invalid scope(s): ${invalidScopes.joinToString(", ")}", state)
177 call.respondRedirect(errorUri)
178 return@post
179 }
53 if (validatedClientId == null) {
54 // id_token_hintなしでredirect_uriを指定した場合はエラー
55 // クライアントを特定できないためリダイレクトURIの検証ができない
56 call.respondOAuthError(OAuthErrorCode.INVALID_REQUEST, "id_token_hint is required when post_logout_redirect_uri is specified")
57 return@get
58 }
59
78 } else {
79 // トークンが無効、既に失効済み、または他のクライアントのトークンの場合
80 // セキュリティ上、成功として扱う(情報漏洩防止)
81 plugin.logger.info("Token revocation completed (token may not exist or already revoked): ${clientData.clientId}")
82 }
83
84 call.respond(HttpStatusCode.OK)
144 val tokenClientId = jwt.getClaim("client_id").asString()
145 if (tokenClientId != clientId) {
146 // 他のクライアントのトークンは失効できないが、成功として扱う(情報漏洩防止)
147 plugin.logger.warning("Attempted to revoke token of different client: requested=$clientId, token=$tokenClientId")
148 return true
149 }
150
128 if (codeVerifier == null) add("code_verifier")
129 }
130 if (missingParams.isNotEmpty()) {
131 call.respondOAuthError(OAuthErrorCode.INVALID_REQUEST, "Missing required parameters: ${missingParams.joinToString(", ")}")
132 return@post
133 }
134
202 .withExpiresAt(Date(System.currentTimeMillis() + EXPIRES_IN * 1_000.toLong()))
203 .withIssuedAt(Date(System.currentTimeMillis()))
204 .withJWTId(UUID.randomUUID().toString())
205 .withClaim("client_id", clientId).withClaim("playerUniqueId", data.uniqueId.toString()).withClaim("scope", data.scope).withClaim("state", data.state).withClaim("scope", data.scope)
206 .withClaim("token_type", "token").sign(JwtProvider.algorithm)
207
208 private fun issueRefreshToken(
78
79 // HTTPプロトコルで/v1/tracesが含まれている場合は警告
80 if (exporter.protocol == OtlpExporterProtocol.HTTP && exporter.endpoint.contains("/v1/traces")) {
81 plugin.logger.warning("HTTP endpoint contains '/v1/traces' suffix - this will be automatically removed (SDK appends it)")
82 }
83 }
84
14 * @return 作成されたRegisterHandler
15 */
16 abstract fun createHandler(plugin : JavaPlugin): RegisterHandler
17 }
8 @Retention(AnnotationRetention.RUNTIME)
9 annotation class DeleteMapping(
10 val value: String
11 )
4 @Retention(AnnotationRetention.RUNTIME)
5 annotation class GetMapping(
6 val value: String
7 )
12 annotation class HttpHandler(
13 val path: String,
14 val method: HttpMethod = HttpMethod.GET
15 )
2
3 @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
4 @Retention(AnnotationRetention.RUNTIME)
5 annotation class Permission(val value: String)
4 @Retention(AnnotationRetention.RUNTIME)
5 annotation class PostMapping(
6 val value: String
7 )
8 @Retention(AnnotationRetention.RUNTIME)
9 annotation class PutMapping(
10 val value: String
11 )
5 */
6 @Target(AnnotationTarget.VALUE_PARAMETER)
7 @Retention(AnnotationRetention.RUNTIME)
8 annotation class RequestBody
56 val status: Int,
57 val body: String?,
58 val headers: Map<String, String> = mapOf()
59 )
10 val status: HttpStatus,
11 override val message: String,
12 val details: Map<String, Any> = mapOf()
13 ) : RuntimeException(message)
12 NOT_FOUND(404),
13 INTERNAL_SERVER_ERROR(500)
14 // 必要に応じて他のステータスコードを追加してください
15 }
29 )
30 }
31 }
32 }
17 companion object {
18 fun fromLocation(location: Location) = LocationData(location.world.name, location.x, location.y, location.z, location.yaw, location.pitch)
19 }
20 }
6 data class PluginsData(
7 val provider : String,
8 val plugins : List<String>
9 )
27 override fun serialize(encoder: Encoder, value: UUID) {
28 encoder.encodeString(value.toString())
29 }
30 }
115 return RegisterHandlerImpl(context)
116 }
117
118 }
69 }
70 }
71 }
72 }
28 sender.sendMessage("Reloaded")
29 }
30
31 }
6 val uuid = varchar("uuid", 36)
7 val password = varchar("password", 255)
8 override val primaryKey = PrimaryKey(uuid)
9 }
10 val realm: String = "morino.party",
11 val privateKeyFile: String = "privateKey.pem",
12 val keyId: @Serializable(with = UUIDSerializer::class) UUID
13 )
6 data class OAuthConfigData(
7 val applicationName: String,
8 val logoUrl: String,
9 )
2
3 interface FileLoaderInterface {
4 fun load()
5 }
10 protected val plugin: MineAuth by inject()
11 protected abstract val configFile: File
12 abstract override fun load()
13 }
24 single { configData }
25 })
26 }
27 }
23 single { configData }
24 })
25 }
26 }
30 }
31 }
32 }
33 }
29 }
30 }
31 }
32 }
10 abstract val name: String
11
12 open fun initialize() {}
13 }
14 it.initialize()
15 }
16 }
17 }
27 return MiniMessage.miniMessage().serialize(this)
28 }
29
30 }
13 return Bukkit.getOfflinePlayer(this)
14 }
15
16 }
16 block.run()
17 }
18 }
19 }
31
32 return syncCoroutine!!
33 }
34 }
16 plugin.server.scheduler.runTask(plugin, block)
17 }
18 }
19 }
7 get() = DispatcherContainer.async
8
9 val Dispatchers.minecraft: CoroutineContext
10 get() = DispatcherContainer.sync
3 enum class JwtCompleteCode(val code: String) {
4 USER_TOKEN("user-oauth-token"),
5 SERVICE_TOKEN("service-oauth-token")
6 }
19 val scope: String? = null,
20 // 注: stateはRFC 6749 Section 5.1により、トークンレスポンスには含めない
21 // stateはリダイレクト時(認可コードと共に)のみ返却される
22 )
11 oauthRouter()
12 loginRouter()
13 }
14 }
13 }
14 }
15 }
16 }
18 PLAYER_NOT_FOUND,
19 PLAYER_NOT_REGISTERED,
20 INVALID_PASSWORD
21 }
1 package party.morino.mineauth.core.web.router.auth.data
2
3 class OIDCConfigData
219 return@post
220 }
221 }
222 }
49 now - data.authTime > maxAgeMs
50 }
51 }
52 }
49 override fun validateClientAndRedirectUri(clientData: ClientData, redirectUri: String): Boolean {
50 return OAuthValidation.validateRedirectUri(clientData, redirectUri)
51 }
52 }
261
262 return clientData.right()
263 }
264 }
14 userRouter()
15 }
16 }
17 }
9 playersRouter()
10 pluginsRoutes()
11 }
12 }
6 fun Route.userRouter() {
7
8 }
9 }
12 call.respond(players)
13 }
14 }
15 }
13
14 }
15 }
16 }
15 // Vault, QuickShop-Hikariはアドオン方式に移行したため、
16 // RegisterHandler API経由で登録される(vault-addon, quickshop-hikari-addon参照)
17 }
18 }
13 * @param plugin ハンドラーを作成するプラグインのインスタンス
14 * @return 作成されたRegisterHandler
15 */
16 abstract fun createHandler(plugin : JavaPlugin): RegisterHandler
17 }
74 /**
75 * 入力値のバリデーション
76 */
77 private fun validate(
78 clientName: String,
79 clientType: String,
80 redirectUri: String
52 /**
53 * 入力値のバリデーション
54 */
55 private fun validate(serviceName: String): Either<String, Unit> {
56 // サービス名のチェック
57 if (serviceName.isBlank()) {
58 return Either.Left("サービス名を入力してください")
46 * @param playerUuid プレイヤーのUUID
47 * @return グループ名のリスト(LuckPerms未使用時は空リスト)
48 */
49 suspend fun getPlayerGroups(playerUuid: UUID): List<String> {
50 if (!available) return emptyList()
51
52 // まずキャッシュからユーザー情報を取得試行(オンラインプレイヤーの場合は高速)
172 * 戻り値の型からレスポンススキーマを生成する
173 * Unitの場合はnullを返す(レスポンスボディなし)
174 */
175 fun generateResponseSchema(returnType: KType): Schema? {
176 val classifier = returnType.classifier as? KClass<*>
177 ?: return null
178
71 * @param method 対象のメソッド
72 * @return HTTPメソッドとパスのペア、アノテーションがない場合はnull
73 */
74 private fun extractHttpMapping(method: KFunction<*>): Pair<HttpMethodType, String>? {
75 // @GetMappingをチェック
76 method.findAnnotation<GetMapping>()?.let {
77 return HttpMethodType.GET to it.value
22 */
23 class AccessPlayerParser : ParameterParser<Any?> {
24
25 override suspend fun parse(
26 call: ApplicationCall,
27 paramInfo: ParameterInfo
28 ): Either<ResolveError, Any?> {
166 * @param paramInfo アクセスプレイヤー情報
167 * @return Player または null
168 */
169 private suspend fun resolveAccessPlayer(
170 call: ApplicationCall,
171 paramInfo: ParameterInfo.AccessPlayer
172 ): Any? {
73 * @param token 検証するトークン(JWT形式)
74 * @return active=true(メタデータ付き)または active=false
75 */
76 private suspend fun introspectToken(token: String): IntrospectionResponse {
77 return try {
78 // 署名・有効期限を検証(JwtProviderのキャッシュ済みverifierを使用)
79 val jwt = try {
17
18 object OAuthService : AuthenticationService, KoinComponent {
19
20 override suspend fun authenticateUser(username: String, password: String): AuthenticationResult {
21 val offlinePlayer = Bukkit.getOfflinePlayer(username)
22 if (!offlinePlayer.hasPlayedBefore()) {
23 return AuthenticationResult.Failed(AuthenticationError.PLAYER_NOT_FOUND)
131 *
132 * @return Pair(client_id, client_secret) または null(ヘッダーが存在しない/不正な場合)
133 */
134 fun RoutingContext.extractBasicCredentials(): Pair<String, String>? {
135 val authHeader = call.request.header(HttpHeaders.Authorization) ?: return null
136 if (!authHeader.startsWith("Basic ", ignoreCase = true)) return null
137 return try {
154 * @param formParameters フォームパラメータ
155 * @return 抽出されたクレデンシャル、または認証エラー
156 */
157 fun RoutingContext.extractClientCredentials(
158 formParameters: Parameters
159 ): Either<ClientAuthFailure, ClientCredentials> {
160 val basicCredentials = extractBasicCredentials()
191 * @param credentials 抽出済みのクレデンシャル
192 * @return 認証済みのConfidentialClientData、または認証エラー
193 */
194 fun authenticateConfidentialClient(
195 credentials: ClientCredentials
196 ): Either<ClientAuthFailure, ClientData.ConfidentialClientData> {
197 // Confidentialクライアントはclient_secret必須
232 * @param credentials 抽出済みのクレデンシャル
233 * @return 認証済みのClientData、または認証エラー
234 */
235 fun authenticateClient(
236 credentials: ClientCredentials
237 ): Either<ClientAuthFailure, ClientData> {
238 // クライアントデータをDBから取得
128 * @param clientId リクエスト元のクライアントID
129 * @return 登録が成功した場合true
130 */
131 private suspend fun registerRevocation(
132 jwt: DecodedJWT,
133 tokenTypeHint: String?,
134 clientId: String
237 * @param refreshToken リフレッシュトークン(JWT形式)
238 * @return 成功時は検証済みデータ、失敗時はnull
239 */
240 private fun verifyAndDecodeRefreshToken(refreshToken: String): VerifiedRefreshToken? {
241 return try {
242 // JWT署名と有効期限を検証(JwtProviderのキャッシュ済みverifierを使用)
243 val jwt = JwtProvider.verifier.verify(refreshToken)
44 * @return 初期化されたOpenTelemetryインスタンス
45 */
46 @Synchronized
47 fun initialize(config: ObservabilityConfig): OpenTelemetry {
48 // 既に初期化済みの場合は一度シャットダウンして再初期化
49 if (openTelemetry != null) {
50 plugin.logger.info("Reinitializing OpenTelemetry with new configuration")
39 * @return 設定済みのKoinモジュール
40 */
41 fun pluginModuleWithKtorSupport(
42 config: KtorSupportConfig = KtorSupportConfig()
43 ) = module {
44 // ルート管理
45 single { PluginRouteRegistry() }
74 * @return 同じRegisterHandlerインスタンス(チェーン呼び出し用)
75 */
76 fun RegisterHandler.installKtorSupport(
77 config: KtorSupportConfig = KtorSupportConfig()
78 ): RegisterHandler {
79 // 現在は設定の検証のみ行う
80 // 将来的にはここでカスタム設定を適用可能
108 */
109 fun getClientData(clientId: String): ClientData {
110 return runBlocking { getClientDataFromDb(clientId) }
111 ?: throw IllegalStateException("Client not found: $clientId")
112 }
113
114 /**
1 package party.morino.mineauth.core.plugin
2
3 import io.ktor.server.routing.*
4 import org.koin.core.component.KoinComponent
5 import java.util.concurrent.ConcurrentHashMap
6
4 import arrow.core.raise.either
5 import arrow.core.raise.ensure
6 import org.koin.core.component.KoinComponent
7 import party.morino.mineauth.api.annotations.*
8 import party.morino.mineauth.api.http.HttpMethod
9 import kotlin.reflect.KClass
10 import kotlin.reflect.KFunction
3 import arrow.core.Either
4 import arrow.core.raise.either
5 import arrow.core.raise.ensure
6 import io.ktor.server.application.*
7 import io.ktor.server.auth.*
8 import io.ktor.server.auth.jwt.*
9 import org.bukkit.Bukkit
4 import arrow.core.raise.either
5 import arrow.core.raise.ensure
6 import io.ktor.server.application.*
7 import io.ktor.server.auth.*
8 import io.ktor.server.auth.jwt.*
9 import org.bukkit.Bukkit
10 import org.koin.core.component.KoinComponent
5 import arrow.core.raise.ensure
6 import io.ktor.server.application.*
7 import io.ktor.server.auth.*
8 import io.ktor.server.auth.jwt.*
9 import org.bukkit.Bukkit
10 import org.koin.core.component.KoinComponent
11 import java.util.*
1 package party.morino.mineauth.core.plugin.route
2
3 import io.ktor.server.auth.*
4 import io.ktor.server.routing.*
5 import org.koin.core.component.KoinComponent
6 import party.morino.mineauth.core.plugin.PluginContext
1 package party.morino.mineauth.core.plugin.route
2
3 import io.ktor.server.auth.*
4 import io.ktor.server.routing.*
5 import org.koin.core.component.KoinComponent
6 import party.morino.mineauth.core.plugin.PluginContext
7 import party.morino.mineauth.core.plugin.annotation.EndpointMetadata
1 package party.morino.mineauth.core.web
2
3 import com.auth0.jwk.JwkProviderBuilder
4 import io.ktor.http.*
5 import io.ktor.serialization.kotlinx.json.*
6 import io.ktor.server.application.*
7 import io.ktor.server.auth.*
2
3 import com.auth0.jwk.JwkProviderBuilder
4 import io.ktor.http.*
5 import io.ktor.serialization.kotlinx.json.*
6 import io.ktor.server.application.*
7 import io.ktor.server.auth.*
8 import io.ktor.server.auth.jwt.*
3 import com.auth0.jwk.JwkProviderBuilder
4 import io.ktor.http.*
5 import io.ktor.serialization.kotlinx.json.*
6 import io.ktor.server.application.*
7 import io.ktor.server.auth.*
8 import io.ktor.server.auth.jwt.*
9 import io.ktor.server.engine.*
4 import io.ktor.http.*
5 import io.ktor.serialization.kotlinx.json.*
6 import io.ktor.server.application.*
7 import io.ktor.server.auth.*
8 import io.ktor.server.auth.jwt.*
9 import io.ktor.server.engine.*
10 import io.ktor.server.http.content.*
5 import io.ktor.serialization.kotlinx.json.*
6 import io.ktor.server.application.*
7 import io.ktor.server.auth.*
8 import io.ktor.server.auth.jwt.*
9 import io.ktor.server.engine.*
10 import io.ktor.server.http.content.*
11 import io.ktor.server.jetty.jakarta.*
6 import io.ktor.server.application.*
7 import io.ktor.server.auth.*
8 import io.ktor.server.auth.jwt.*
9 import io.ktor.server.engine.*
10 import io.ktor.server.http.content.*
11 import io.ktor.server.jetty.jakarta.*
12 import io.ktor.server.metrics.micrometer.*
7 import io.ktor.server.auth.*
8 import io.ktor.server.auth.jwt.*
9 import io.ktor.server.engine.*
10 import io.ktor.server.http.content.*
11 import io.ktor.server.jetty.jakarta.*
12 import io.ktor.server.metrics.micrometer.*
13 import io.ktor.server.plugins.calllogging.*
8 import io.ktor.server.auth.jwt.*
9 import io.ktor.server.engine.*
10 import io.ktor.server.http.content.*
11 import io.ktor.server.jetty.jakarta.*
12 import io.ktor.server.metrics.micrometer.*
13 import io.ktor.server.plugins.calllogging.*
14 import io.ktor.server.plugins.contentnegotiation.*
9 import io.ktor.server.engine.*
10 import io.ktor.server.http.content.*
11 import io.ktor.server.jetty.jakarta.*
12 import io.ktor.server.metrics.micrometer.*
13 import io.ktor.server.plugins.calllogging.*
14 import io.ktor.server.plugins.contentnegotiation.*
15 import io.ktor.server.request.*
10 import io.ktor.server.http.content.*
11 import io.ktor.server.jetty.jakarta.*
12 import io.ktor.server.metrics.micrometer.*
13 import io.ktor.server.plugins.calllogging.*
14 import io.ktor.server.plugins.contentnegotiation.*
15 import io.ktor.server.request.*
16 import io.ktor.server.response.*
11 import io.ktor.server.jetty.jakarta.*
12 import io.ktor.server.metrics.micrometer.*
13 import io.ktor.server.plugins.calllogging.*
14 import io.ktor.server.plugins.contentnegotiation.*
15 import io.ktor.server.request.*
16 import io.ktor.server.response.*
17 import io.ktor.server.routing.*
12 import io.ktor.server.metrics.micrometer.*
13 import io.ktor.server.plugins.calllogging.*
14 import io.ktor.server.plugins.contentnegotiation.*
15 import io.ktor.server.request.*
16 import io.ktor.server.response.*
17 import io.ktor.server.routing.*
18 import io.micrometer.prometheusmetrics.PrometheusConfig
13 import io.ktor.server.plugins.calllogging.*
14 import io.ktor.server.plugins.contentnegotiation.*
15 import io.ktor.server.request.*
16 import io.ktor.server.response.*
17 import io.ktor.server.routing.*
18 import io.micrometer.prometheusmetrics.PrometheusConfig
19 import io.micrometer.prometheusmetrics.PrometheusMeterRegistry
14 import io.ktor.server.plugins.contentnegotiation.*
15 import io.ktor.server.request.*
16 import io.ktor.server.response.*
17 import io.ktor.server.routing.*
18 import io.micrometer.prometheusmetrics.PrometheusConfig
19 import io.micrometer.prometheusmetrics.PrometheusMeterRegistry
20 import io.opentelemetry.instrumentation.ktor.v3_0.KtorServerTelemetry
19 import io.micrometer.prometheusmetrics.PrometheusMeterRegistry
20 import io.opentelemetry.instrumentation.ktor.v3_0.KtorServerTelemetry
21 import org.slf4j.event.Level
22 import io.ktor.server.velocity.*
23 import org.apache.velocity.runtime.RuntimeConstants
24 import org.apache.velocity.runtime.resource.loader.FileResourceLoader
25 import org.koin.core.component.KoinComponent
1 package party.morino.mineauth.core.web.router.auth
2
3 import io.ktor.server.routing.*
4 import party.morino.mineauth.core.web.router.auth.LoginRouter.loginRouter
5 import party.morino.mineauth.core.web.router.auth.WellKnownRouter.wellKnownRouter
6 import party.morino.mineauth.core.web.router.auth.oauth.OAuthRouter.oauthRouter
1 package party.morino.mineauth.core.web.router.auth
2
3 import io.ktor.server.response.*
4 import io.ktor.server.routing.*
5 import io.ktor.server.velocity.*
6
1 package party.morino.mineauth.core.web.router.auth
2
3 import io.ktor.server.response.*
4 import io.ktor.server.routing.*
5 import io.ktor.server.velocity.*
6
7 object LoginRouter {
2
3 import io.ktor.server.response.*
4 import io.ktor.server.routing.*
5 import io.ktor.server.velocity.*
6
7 object LoginRouter {
8 fun Route.loginRouter() {
1 package party.morino.mineauth.core.web.router.auth
2
3 import io.ktor.http.*
4 import io.ktor.server.response.*
5 import io.ktor.server.routing.*
6 import org.koin.core.component.KoinComponent
1 package party.morino.mineauth.core.web.router.auth
2
3 import io.ktor.http.*
4 import io.ktor.server.response.*
5 import io.ktor.server.routing.*
6 import org.koin.core.component.KoinComponent
7 import org.koin.core.component.inject
2
3 import io.ktor.http.*
4 import io.ktor.server.response.*
5 import io.ktor.server.routing.*
6 import org.koin.core.component.KoinComponent
7 import org.koin.core.component.inject
8 import party.morino.mineauth.core.file.data.MineAuthConfig
1 package party.morino.mineauth.core.web.router.auth.oauth
2
3 import io.ktor.http.*
4 import io.ktor.server.request.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
1 package party.morino.mineauth.core.web.router.auth.oauth
2
3 import io.ktor.http.*
4 import io.ktor.server.request.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import io.ktor.server.velocity.*
2
3 import io.ktor.http.*
4 import io.ktor.server.request.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import io.ktor.server.velocity.*
8 import java.security.SecureRandom
3 import io.ktor.http.*
4 import io.ktor.server.request.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import io.ktor.server.velocity.*
8 import java.security.SecureRandom
9 import org.koin.core.component.KoinComponent
4 import io.ktor.server.request.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import io.ktor.server.velocity.*
8 import java.security.SecureRandom
9 import org.koin.core.component.KoinComponent
10 import org.koin.core.component.inject
1 package party.morino.mineauth.core.web.router.auth.oauth
2
3 import com.auth0.jwt.exceptions.JWTVerificationException
4 import io.ktor.http.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import org.koin.core.component.KoinComponent
2
3 import com.auth0.jwt.exceptions.JWTVerificationException
4 import io.ktor.http.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import org.koin.core.component.KoinComponent
8 import org.koin.core.component.inject
3 import com.auth0.jwt.exceptions.JWTVerificationException
4 import io.ktor.http.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import org.koin.core.component.KoinComponent
8 import org.koin.core.component.inject
9 import party.morino.mineauth.core.MineAuth
3 import arrow.core.Either
4 import com.auth0.jwt.exceptions.JWTVerificationException
5 import com.auth0.jwt.exceptions.TokenExpiredException
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.response.*
9 import io.ktor.server.routing.*
4 import com.auth0.jwt.exceptions.JWTVerificationException
5 import com.auth0.jwt.exceptions.TokenExpiredException
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.response.*
9 import io.ktor.server.routing.*
10 import org.koin.core.component.KoinComponent
5 import com.auth0.jwt.exceptions.TokenExpiredException
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.response.*
9 import io.ktor.server.routing.*
10 import org.koin.core.component.KoinComponent
11 import org.koin.core.component.inject
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.response.*
9 import io.ktor.server.routing.*
10 import org.koin.core.component.KoinComponent
11 import org.koin.core.component.inject
12 import party.morino.mineauth.core.MineAuth
1 package party.morino.mineauth.core.web.router.auth.oauth
2
3 import io.ktor.http.*
4 import io.ktor.server.request.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
1 package party.morino.mineauth.core.web.router.auth.oauth
2
3 import io.ktor.http.*
4 import io.ktor.server.request.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import kotlinx.serialization.SerialName
2
3 import io.ktor.http.*
4 import io.ktor.server.request.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import kotlinx.serialization.SerialName
8 import kotlinx.serialization.Serializable
3 import io.ktor.http.*
4 import io.ktor.server.request.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import kotlinx.serialization.SerialName
8 import kotlinx.serialization.Serializable
9 import org.koin.core.component.KoinComponent
1 package party.morino.mineauth.core.web.router.auth.oauth
2
3 import io.ktor.server.routing.*
4 import org.koin.core.component.KoinComponent
5 import party.morino.mineauth.core.web.router.auth.data.AuthorizedData
6 import java.util.concurrent.ConcurrentHashMap
3 import arrow.core.Either
4 import arrow.core.left
5 import arrow.core.right
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.routing.*
9 import kotlinx.coroutines.runBlocking
4 import arrow.core.left
5 import arrow.core.right
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.routing.*
9 import kotlinx.coroutines.runBlocking
10 import org.koin.core.component.KoinComponent
5 import arrow.core.right
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.routing.*
9 import kotlinx.coroutines.runBlocking
10 import org.koin.core.component.KoinComponent
11 import party.morino.mineauth.core.web.components.auth.ClientData
1 package party.morino.mineauth.core.web.router.auth.oauth
2
3 import io.ktor.server.auth.*
4 import io.ktor.server.auth.jwt.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
1 package party.morino.mineauth.core.web.router.auth.oauth
2
3 import io.ktor.server.auth.*
4 import io.ktor.server.auth.jwt.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import org.koin.core.component.KoinComponent
2
3 import io.ktor.server.auth.*
4 import io.ktor.server.auth.jwt.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import org.koin.core.component.KoinComponent
8 import org.koin.core.component.inject
3 import io.ktor.server.auth.*
4 import io.ktor.server.auth.jwt.*
5 import io.ktor.server.response.*
6 import io.ktor.server.routing.*
7 import org.koin.core.component.KoinComponent
8 import org.koin.core.component.inject
9 import party.morino.mineauth.core.file.data.MineAuthConfig
3 import arrow.core.Either
4 import com.auth0.jwt.exceptions.JWTVerificationException
5 import com.auth0.jwt.interfaces.DecodedJWT
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.response.*
9 import io.ktor.server.routing.*
4 import com.auth0.jwt.exceptions.JWTVerificationException
5 import com.auth0.jwt.interfaces.DecodedJWT
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.response.*
9 import io.ktor.server.routing.*
10 import kotlin.coroutines.cancellation.CancellationException
5 import com.auth0.jwt.interfaces.DecodedJWT
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.response.*
9 import io.ktor.server.routing.*
10 import kotlin.coroutines.cancellation.CancellationException
11 import org.koin.core.component.KoinComponent
6 import io.ktor.http.*
7 import io.ktor.server.request.*
8 import io.ktor.server.response.*
9 import io.ktor.server.routing.*
10 import kotlin.coroutines.cancellation.CancellationException
11 import org.koin.core.component.KoinComponent
12 import org.koin.core.component.inject
2
3 import arrow.core.Either
4 import com.auth0.jwt.JWT
5 import io.ktor.http.*
6 import io.ktor.server.request.*
7 import io.ktor.server.response.*
8 import io.ktor.server.routing.*
3 import arrow.core.Either
4 import com.auth0.jwt.JWT
5 import io.ktor.http.*
6 import io.ktor.server.request.*
7 import io.ktor.server.response.*
8 import io.ktor.server.routing.*
9 import org.koin.core.component.KoinComponent
4 import com.auth0.jwt.JWT
5 import io.ktor.http.*
6 import io.ktor.server.request.*
7 import io.ktor.server.response.*
8 import io.ktor.server.routing.*
9 import org.koin.core.component.KoinComponent
10 import org.koin.core.component.get
5 import io.ktor.http.*
6 import io.ktor.server.request.*
7 import io.ktor.server.response.*
8 import io.ktor.server.routing.*
9 import org.koin.core.component.KoinComponent
10 import org.koin.core.component.get
11 import org.koin.core.component.inject
1 package party.morino.mineauth.core.web.router.common
2
3 import io.ktor.server.routing.*
4 import party.morino.mineauth.core.web.router.common.ServerRouter.serverRouter
5 import party.morino.mineauth.core.web.router.common.UserRouter.userRouter
6
1 package party.morino.mineauth.core.web.router.common
2
3 import io.ktor.server.routing.*
4 import party.morino.mineauth.core.web.router.common.server.PlayersRouter.playersRouter
5 import party.morino.mineauth.core.web.router.common.server.PluginsRouter.pluginsRoutes
6
1 package party.morino.mineauth.core.web.router.common
2
3 import io.ktor.server.routing.*
4
5 object UserRouter {
6 fun Route.userRouter() {
1 package party.morino.mineauth.core.web.router.common.server
2
3 import io.ktor.server.response.*
4 import io.ktor.server.routing.*
5 import org.bukkit.Bukkit
6 import party.morino.mineauth.api.model.common.ProfileData
1 package party.morino.mineauth.core.web.router.common.server
2
3 import io.ktor.server.response.*
4 import io.ktor.server.routing.*
5 import org.bukkit.Bukkit
6 import party.morino.mineauth.api.model.common.ProfileData
7
1 package party.morino.mineauth.core.web.router.plugin
2
3 import io.ktor.server.response.*
4 import io.ktor.server.routing.*
5 import party.morino.mineauth.core.integration.IntegrationInitializer
6
1 package party.morino.mineauth.core.web.router.plugin
2
3 import io.ktor.server.response.*
4 import io.ktor.server.routing.*
5 import party.morino.mineauth.core.integration.IntegrationInitializer
6
7 object PluginRouter {