LoginSignup
2
1

More than 1 year has passed since last update.

【Scala】JWT ScalaのNativeだけ使用して簡単に認証を実装してみる

Posted at

この記事はジャンルなしオンラインもくもく会 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)
  }
}
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1