kotlinx-serialization-json

認証ライブラリKotlinJSONシリアライゼーションセキュリティREST API

認証ライブラリ

kotlinx-serialization-json

概要

kotlinx-serialization-jsonは、KotlinのマルチプラットフォームJSON シリアライゼーションライブラリで、認証システムにおけるトークンの安全な処理とJSONデータの型安全な変換を提供します。

詳細

kotlinx-serialization-jsonは、JetBrains社が開発したKotlinプログラミング言語用の公式JSONシリアライゼーションライブラリです。認証システムにおいて、JWTトークンの安全な解析、OAuth2レスポンスの処理、ユーザープロファイル情報のシリアライゼーションなど、JSON形式のデータを型安全に扱う際に重要な役割を果たします。ライブラリは完全にコンパイル時型チェックを提供し、ランタイムエラーを防ぎます。認証における重要な機能として、未知のフィールドの無視、null値の適切な処理、セキュリティ上重要なフィールドの暗黙的な除外などがあります。また、PKCS#7、RFC 6750などの標準準拠や、アクセストークンやリフレッシュトークンの安全な保存に必要なbase64エンコーディングもサポートしています。マルチプラットフォーム対応により、サーバーサイド認証とクライアントサイド認証の両方で一貫したAPIを使用できます。

メリット・デメリット

メリット

  • 型安全性: コンパイル時の型チェックによる認証トークンの安全な処理
  • セキュリティ特化: 未知フィールド無視やnull値処理による堅牢性
  • マルチプラットフォーム: JVM、Android、iOS、JS、Nativeでの一貫したAPI
  • パフォーマンス: 軽量で高速なシリアライゼーション処理
  • 標準準拠: JWT、OAuth2、OpenID Connectの標準に完全対応
  • カスタマイズ可能: 認証プロバイダー固有の要件に対応可能
  • 型推論サポート: Kotlinの型推論による開発体験向上

デメリット

  • 学習コスト: Kotlinのアノテーション機能や型システムの理解が必要
  • Kotlin専用: 他のJVM言語からの直接利用は制限的
  • 依存関係: kotlinx-coroutinesなどKotlinエコシステムへの依存
  • 設定の複雑さ: 高度なカスタマイゼーションは設定が煩雑
  • デバッグ難易度: シリアライゼーションエラーの原因特定が困難な場合がある

主要リンク

書き方の例

Hello World(基本的なトークン処理)

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class AuthToken(
    val accessToken: String,
    val tokenType: String = "Bearer",
    val expiresIn: Int,
    val refreshToken: String? = null
)

fun main() {
    val tokenJson = """
        {
            "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
            "token_type": "Bearer",
            "expires_in": 3600,
            "refresh_token": "refresh_token_here"
        }
    """
    
    val token = Json.decodeFromString<AuthToken>(tokenJson)
    println("Access Token: ${token.accessToken}")
    println("Expires In: ${token.expiresIn} seconds")
}

OAuth2レスポンス処理

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class OAuth2Response(
    val accessToken: String,
    val tokenType: String,
    val expiresIn: Int,
    val scope: String? = null,
    val refreshToken: String? = null,
    val idToken: String? = null // OpenID Connect
)

// 未知のフィールドを無視する設定
val secureJson = Json {
    ignoreUnknownKeys = true
    coerceInputValues = true
    isLenient = true
}

fun processOAuth2Response(jsonResponse: String): OAuth2Response {
    return secureJson.decodeFromString<OAuth2Response>(jsonResponse)
}

fun main() {
    val oauthResponse = """
        {
            "access_token": "ya29.a0AfH6SMBqX...",
            "token_type": "Bearer",
            "expires_in": 3599,
            "scope": "openid profile email",
            "refresh_token": "1//0GWZ...",
            "id_token": "eyJhbGciOiJSUzI1NiIs...",
            "unknown_field": "ignored"
        }
    """
    
    val response = processOAuth2Response(oauthResponse)
    println("Token Type: ${response.tokenType}")
    println("Scope: ${response.scope}")
}

セキュアなユーザープロファイル処理

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class UserProfile(
    val id: String,
    val email: String,
    val name: String,
    val picture: String? = null,
    @SerialName("email_verified")
    val emailVerified: Boolean = false,
    val locale: String? = null
)

@Serializable
data class SecureUserSession(
    val userId: String,
    val email: String,
    val roles: List<String>,
    val permissions: Set<String>,
    val sessionId: String,
    val expiresAt: Long,
    @Transient // シリアライゼーションから除外
    val sensitiveData: String? = null
)

fun main() {
    val userJson = """
        {
            "id": "user123",
            "email": "[email protected]",
            "name": "John Doe",
            "picture": "https://example.com/avatar.jpg",
            "email_verified": true,
            "locale": "ja"
        }
    """
    
    val user = Json.decodeFromString<UserProfile>(userJson)
    
    // セキュアなセッション作成
    val session = SecureUserSession(
        userId = user.id,
        email = user.email,
        roles = listOf("user"),
        permissions = setOf("read", "write"),
        sessionId = "session_${System.currentTimeMillis()}",
        expiresAt = System.currentTimeMillis() + 3600000
    )
    
    val sessionJson = Json.encodeToString(session)
    println("Secure Session: $sessionJson")
}

JWTペイロード処理

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.util.Base64

@Serializable
data class JWTHeader(
    val alg: String,
    val typ: String,
    val kid: String? = null
)

@Serializable
data class JWTPayload(
    val sub: String, // Subject
    val iss: String, // Issuer
    val aud: String, // Audience
    val exp: Long,   // Expiration
    val iat: Long,   // Issued At
    val nbf: Long? = null, // Not Before
    val jti: String? = null, // JWT ID
    val scope: String? = null,
    val email: String? = null,
    val name: String? = null
)

object JWTParser {
    private val json = Json {
        ignoreUnknownKeys = true
        coerceInputValues = true
    }
    
    fun parseJWT(token: String): Pair<JWTHeader, JWTPayload> {
        val parts = token.split(".")
        require(parts.size == 3) { "Invalid JWT format" }
        
        val headerJson = Base64.getDecoder().decode(parts[0]).decodeToString()
        val payloadJson = Base64.getDecoder().decode(parts[1]).decodeToString()
        
        val header = json.decodeFromString<JWTHeader>(headerJson)
        val payload = json.decodeFromString<JWTPayload>(payloadJson)
        
        return header to payload
    }
}

fun main() {
    // 実際のJWTトークンの例(簡略化)
    val jwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
    
    try {
        val (header, payload) = JWTParser.parseJWT(jwtToken)
        println("Algorithm: ${header.alg}")
        println("Subject: ${payload.sub}")
        println("Issuer: ${payload.iss}")
        println("Expires: ${payload.exp}")
    } catch (e: Exception) {
        println("JWT parsing failed: ${e.message}")
    }
}

認証エラーハンドリング

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class AuthError(
    val error: String,
    val errorDescription: String? = null,
    @SerialName("error_uri")
    val errorUri: String? = null,
    val state: String? = null
)

@Serializable
data class AuthResult<T>(
    val success: Boolean,
    val data: T? = null,
    val error: AuthError? = null
)

class AuthenticationManager {
    private val json = Json {
        ignoreUnknownKeys = true
        coerceInputValues = true
        explicitNulls = false
    }
    
    fun <T> handleAuthResponse(
        jsonResponse: String,
        dataSerializer: KSerializer<T>
    ): AuthResult<T> {
        return try {
            // 成功レスポンスとして解析を試行
            val data = json.decodeFromJsonElement(
                dataSerializer,
                json.parseToJsonElement(jsonResponse)
            )
            AuthResult(success = true, data = data)
        } catch (e: Exception) {
            // エラーレスポンスとして解析
            try {
                val error = json.decodeFromString<AuthError>(jsonResponse)
                AuthResult(success = false, error = error)
            } catch (parseError: Exception) {
                AuthResult(
                    success = false,
                    error = AuthError(
                        error = "parse_error",
                        errorDescription = "Failed to parse response: ${parseError.message}"
                    )
                )
            }
        }
    }
}

fun main() {
    val authManager = AuthenticationManager()
    
    // 成功レスポンス
    val successResponse = """{"access_token": "token123", "expires_in": 3600}"""
    val result = authManager.handleAuthResponse(
        successResponse,
        AuthToken.serializer()
    )
    
    if (result.success) {
        println("Authentication successful: ${result.data}")
    } else {
        println("Authentication failed: ${result.error}")
    }
    
    // エラーレスポンス
    val errorResponse = """
        {
            "error": "invalid_client",
            "error_description": "Client authentication failed"
        }
    """
    val errorResult = authManager.handleAuthResponse(
        errorResponse,
        AuthToken.serializer()
    )
    
    if (!errorResult.success) {
        println("Error: ${errorResult.error?.error}")
        println("Description: ${errorResult.error?.errorDescription}")
    }
}

認証設定の管理

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class OAuthConfig(
    val clientId: String,
    val clientSecret: String,
    val redirectUri: String,
    val scope: List<String>,
    val authorizationEndpoint: String,
    val tokenEndpoint: String,
    val userInfoEndpoint: String? = null,
    val pkceEnabled: Boolean = true
)

@Serializable
data class AuthenticationSettings(
    val providers: Map<String, OAuthConfig>,
    val defaultProvider: String,
    val sessionTimeout: Long,
    val tokenRefreshThreshold: Long,
    val securitySettings: SecuritySettings
)

@Serializable
data class SecuritySettings(
    val enableCsrfProtection: Boolean = true,
    val enableRateLimiting: Boolean = true,
    val maxLoginAttempts: Int = 5,
    val lockoutDuration: Long = 300000, // 5 minutes
    val requireHttps: Boolean = true
)

fun loadAuthConfiguration(configJson: String): AuthenticationSettings {
    val json = Json {
        ignoreUnknownKeys = true
        explicitNulls = false
        encodeDefaults = false
    }
    
    return json.decodeFromString<AuthenticationSettings>(configJson)
}

fun main() {
    val configJson = """
        {
            "providers": {
                "google": {
                    "client_id": "google_client_id",
                    "client_secret": "google_secret",
                    "redirect_uri": "https://app.example.com/auth/callback",
                    "scope": ["openid", "profile", "email"],
                    "authorization_endpoint": "https://accounts.google.com/o/oauth2/auth",
                    "token_endpoint": "https://oauth2.googleapis.com/token",
                    "user_info_endpoint": "https://www.googleapis.com/oauth2/v2/userinfo",
                    "pkce_enabled": true
                },
                "github": {
                    "client_id": "github_client_id",
                    "client_secret": "github_secret",
                    "redirect_uri": "https://app.example.com/auth/callback",
                    "scope": ["user:email"],
                    "authorization_endpoint": "https://github.com/login/oauth/authorize",
                    "token_endpoint": "https://github.com/login/oauth/access_token"
                }
            },
            "default_provider": "google",
            "session_timeout": 86400000,
            "token_refresh_threshold": 300000,
            "security_settings": {
                "enable_csrf_protection": true,
                "enable_rate_limiting": true,
                "max_login_attempts": 3,
                "lockout_duration": 600000,
                "require_https": true
            }
        }
    """
    
    val config = loadAuthConfiguration(configJson)
    println("Default Provider: ${config.defaultProvider}")
    println("Providers: ${config.providers.keys}")
    println("CSRF Protection: ${config.securitySettings.enableCsrfProtection}")
}