JWT-RS512でクライアントサイドで検証可能なアクセストークン(セッションID)


JWTの利点

  • データベースにアクセスしないでアクセストークンの検証ができる
  • データベースにアクセスしないでアクセストークンからユーザーIDなどを取得できる

JWT-RSの利点

  • 公開鍵を使って、アクセストークンの発行元以外でも有効性の検証やユーザーIDなどの取得ができる

鍵の生成方法

$ openssl genrsa 4096 > prikey.txt
$ # 公開鍵を生成する
$ openssl rsa -pubout < prikey.txt > pubkey.txt
$ # 秘密鍵をJavaから読み込み可能なフォーマットに変換する
$ openssl pkcs8 -topk8 -inform PEM -in prikey.txt -out private_key.txt -nocrypt

Java VMのバージョンについての備考

Macの場合、標準でインストールされているjdk1.8.0_25だと、RSA128までしか使うことができない。
RSA512を使うためには、jdk1.8.0_172をインストールする必要がある。

依存関係(Kotlin / Java)

// build.gradle
dependencies {
  compile 'com.auth0:java-jwt:3.4.0'
}

鍵の読み込み(Kotlin / Java)

import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.Base64


fun loadRSAKeyPair(publicKeyContent: String, privateKeyContent: String): Pair<RSAPublicKey, RSAPrivateKey> {
  // -----BEGIN ... KEY-----, -----END ... KEY-----を除去
  val pub = publicKeyContent.replace("^-+.+?-$".toRegex(RegexOption.MULTILINE), "").replace("\\s".toRegex(RegexOption.MULTILINE), "")
  val pri = privateKeyContent.replace("^-+.+?-$".toRegex(RegexOption.MULTILINE), "").replace("\\s".toRegex(RegexOption.MULTILINE), "")

  val kf = KeyFactory.getInstance("RSA")

  val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.getDecoder().decode(pri))
  val privateKey = kf.generatePrivate(keySpecPKCS8) as RSAPrivateKey

  val keySpecX509 = X509EncodedKeySpec(Base64.getDecoder().decode(pub))
  val publicKey = kf.generatePublic(keySpecX509) as RSAPublicKey
  return Pair(pubKey, privateKey)
}

fun loadRSAPublicKey(publicKeyContent: String): RSAPublicKey {
  val pub = publicKeyContent.replace("^-+.+?-$".toRegex(RegexOption.MULTILINE), "").replace("\\s".toRegex(RegexOption.MULTILINE), "")
  val kf = KeyFactory.getInstance("RSA")
  val keySpecX509 = X509EncodedKeySpec(Base64.getDecoder().decode(pub))
  return kf.generatePublic(keySpecX509) as RSAPublicKey
}

Access Tokenの生成(Kotlin / Java)

import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.JWT

fun generateAccessToken(user: User, issuer: String, publicKeyContent: String, privateKeyContent: String): String {
  val (publicKey, privateKey) = loadRSAKeyPair(publicKeyContent, privateKeyContent)
  val algorithm = Algorithm.RSA512(publicKey, privateKey)
  // 有効期限 (Longにしないとオーバーフローする)
  val expireMilliSec = 60L * 60L * 1L * 1000L
  return JWT.create()
          .withIssuer(issuer)          
          .withExpiresAt(Date(Date().time + expireMilliSec))
          .withClaim("uid", user.id)
          .sign(algorithm)
}

Access Tokenの検証(Kotlin / Java)

import com.auth0.jwt.exceptions.TokenExpiredException
import com.auth0.jwt.exceptions.JWTVerificationException

private fun getUserId(accessToken:String):Long {
  val jwt = decodeAccessToken(accessToken, issuer, publicKeyContent) ?: return null
  return jwt.getClaim("uid")?.asLong()
}

private fun decodeAccessToken(accessToken: String, issuer: String, publicKeyContent: String): DecodedJWT? {
  val publicKey = loadRSAPublicKey(publicKeyContent)
  val algorithm = Algorithm.RSA512(publicKey, null)
  val verifier = JWT.require(algorithm)
          .withIssuer(issuer)
          .build()
  try {
      return verifier.verify(accessToken)
  } catch (ex: TokenExpiredException) {
      return null
  } catch (ex: JWTVerificationException) {
      return null
  }
}

依存関係(Node.js)

$ yarn add jsonwebtoken

Access Tokenの検証(Node.js)

const jwt = require('jsonwebtoken')
const util = require('util')

const verifyAccessToken = async (accessToken) => {
  if (!accessToken) { return null }
  try {
    const token = await util.promisify(jwt.verify)(accessToken, publicKeyContent, {
      algorithms: 'RS512'
    })
    return {
      userId: token.uid,
    }
  } catch (ex) {
    return null
  }
}

See also