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
}
}