この記事はジャンルなしオンラインもくもく会 Advent Calendar 2021の23日目の記事です。ジャンルなしということなので、今回はScalaについて書いてみます。
概要
PlayFrameworkには様々認証の仕組みが用意されています。そこまで仰仰しい認証の仕組みは使わず、jwtでtokenの認証だけ出来れば良いという実装を行う場合、どうすればよいかというのを考えてみました。
今回はJWT ScalaのNativeの機能を使って、簡単にtoken認証する方法を紹介したいと思います。
対応方針
- 認証のライブラリは、JWT ScalaのNativeを使用します。Nativeはフレームワークなどに依存せず、Jwt単体で使用できる機能です。
- Webのフレームワークには、PlayFrameworkを使用します。PlayFramework自体の認証機能は使いません。JWT Scalaの認証処理の組み込みは、Implementing Authentication on Play Frameworkの「Starting raw」の項を参考に行います。
実装サンプル
<ログイン処理(jwtトークンを生成)>
UseController.scala
@Singleton
class UserController @Inject()(val controllerComponents: ControllerComponents) extends BaseController with I18nSupport {
import model.api.user.LoginByAccountCodeRequest.loginByAccountCodeRequestForm
def loginByAccountCode() = Action { implicit request: Request[AnyContent] =>
loginByAccountCodeRequestForm.bindFromRequest().fold(
errors => BadRequest(errors.errorsAsJson),
form => {
// Googleの認証コードを使って認証する処理を想定(詳細は割愛)
authByGoogleAuthCode(form.accountCode).fold(InternalServerError("Failed authentication"))(u =>
// 認証OKだったらjwtトークンを返す
Ok(Json.toJson(LoginResponse(
u.name,
u.getToken() // 後述で記載の「AuthUser」でjwtのトークン取得処理を実装
)))
)
}
)
}
}
AuthUser.scala
object AuthUser {
implicit val jsonWrites = Json.writes[AuthUser]
implicit val jsonReads = Json.reads[AuthUser]
}
case class AuthUser(id: String = "", name: String = "") {
val config = ConfigFactory.load()
implicit val clock: Clock = Clock.systemUTC
// jwtトークンの生成
def getToken(): String = {
// claimはJson形式で保存
val claim = Json.toJson(this)
Jwt.encode(
JwtClaim(s"$claim").expiresIn(7776000), // 期限は3ヶ月
config.getString("jwt.secret"), // confファイルから設定したsecretを取得
JwtAlgorithm.HS256)
}
}
<認証処理(jwtトークンの検証)>
UseController.scala
@Singleton
class UserController @Inject()(val controllerComponents: ControllerComponents) extends BaseController with I18nSupport {
import useCase.AuthUserUseCase._
def loginCheckByToken() = Action { implicit request: Request[AnyContent] =>
// 後述のAuthUserUseCase.scalaで渡されたトークンを検証
withUser { authUser => {
Ok(Json.toJson(LoginResponse(
authUser.name,
authUser.getToken()
)))
}
}
}
}
UseController.scala
object AuthUserUseCase {
val config = ConfigFactory.load()
def withUser[T](block: AuthUser => Result)(implicit request: Request[AnyContent]): Result = {
// リクエストのAuthorizationヘッダーへ、トークンを設定している前提
val authTokenOpt = request.headers.get("Authorization")
authTokenOpt
.flatMap(token => {
AuthUser.getAuthUserFromToken(token) // 後述で記載の「AuthUser」でjwtトークンからユーザ情報取得を実装
})
.map(block)
.getOrElse(Unauthorized)
}
}
AuthUser.scala
object AuthUser {
val config = ConfigFactory.load()
implicit val jsonWrites = Json.writes[AuthUser]
implicit val jsonReads = Json.reads[AuthUser]
// tokenからユーザ情報を取得
def getAuthUserFromToken(token: String): Option[AuthUser] = {
// confファイルから設定したsecretを取得
val secret = config.getString("jwt.secret")
if (!Jwt.isValid(token, secret, Seq(JwtAlgorithm.HS256))) {
return None
}
val decodedJwt = Jwt.decode(token, secret, Seq(JwtAlgorithm.HS256))
decodedJwt.toOption.flatMap(claim => {
// claimに保存されているJsonをパース
Json.parse(claim.content).validate[AuthUser].asOpt
})
}
}
case class AuthUser(id: String = "",
name: String = "",
authMethod: String = "",
) {
val config = ConfigFactory.load()
implicit val clock: Clock = Clock.systemUTC
def getToken(): String = {
val claim = Json.toJson(this)
// 期限は3ヶ月
Jwt.encode(
JwtClaim(s"$claim").expiresIn(7776000),
config.getString("jwt.secret"),
JwtAlgorithm.HS256)
}
}