Fusic Tech Blog

Fusicエンジニアによる技術ブログ

goaで作ったAPIサーバにJWT認証を追加する
2024/03/25

goaで作ったAPIサーバにJWT認証を追加する

Fusicの岡嵜です。

私が担当した前回の記事でgoaというフレームワークを使ってgolang製のAPIサーバを作りました。

今回は、JWT認証を兼ね備えたAPIサーバを作ってみることとします。

参考資料

goaの公式リポジトリにJWT認証のサンプルがあるのでこれを参考とします。
https://github.com/goadesign/examples/blob/master/security/jwt.go

designを定義

前回同様、design.goを記述します。

package design
 
 import (
 . "github.com/goadesign/goa/design"
 . "github.com/goadesign/goa/design/apidsl"
 )
 
 var \_ = API("Secure", func() {
 Scheme("http")
 Host("localhost:8080")
 BasePath("/api/v1")
 })
 
 // BasicAuth defines a security scheme using basic authentication. The scheme protects the "signin"
 // action used to create JWTs.
 var SigninBasicAuth = BasicAuthSecurity("SigninBasicAuth")
 
 // JWT defines a security scheme using JWT. The scheme uses the "Authorization" header to lookup
 // the token. It also defines then scope "api".
 var JWT = JWTSecurity("jwt", func() {
 Header("Authorization")
 Scope("api:access", "API access") // Define "api:access" scope
 })
 
 // Resource jwt uses the JWTSecurity security scheme.
 var \_ = Resource("jwt", func() {
 DefaultMedia(SuccessMedia)
 
 Security(JWT, func() { // Use JWT to auth requests to this endpoint
 Scope("api:access") // Enforce presence of "api" scope in JWT claims.
 })
 
 Action("signin", func() {
 Security(SigninBasicAuth)
 Routing(POST("/jwt/signin"))
 Response(NoContent, func() {
 Headers(func() {
 Header("Authorization", String, "Generated JWT")
 })
 })
 Response(Unauthorized)
 })
 
 Action("secure", func() {
 Routing(GET("/jwt"))
 Response(OK)
 Response(Unauthorized)
 })
 })
 
 var SuccessMedia = MediaType("application/vnd.goa.examples.security.success", func() {
 Attributes(func() {
 Attribute("ok", Boolean, "Always true")
 Required("ok")
 })
 View("default", func() {
 Attribute("ok")
 })
 })

定義しているAPIは2つです。

POST /jwt/signin

このエンドポイントへbasic認証付きでPOSTすることでtokenを発行します。

GET /jwt

このエンドポイントへtoken付きでGETすることで、ok:trueを返します。
tokenがない場合や不正な場合はok:falseを返します。

以下コマンドを実行してコードを自動生成します。

$ goagen bootstrap -d github.com/yuuu/auth\_api/design

middlewareを追加

今回、basic認証とJWTという2つのミドルウェアを使用するため、自動生成されたmain.goのmain()に以下を追記します。

package main
 
 import (
 "context"
 "io/ioutil"
 "net/http"
 "os"
 
 jwtgo "github.com/dgrijalva/jwt-go"
 "github.com/goadesign/goa"
 "github.com/goadesign/goa/middleware"
 "github.com/goadesign/goa/middleware/security/jwt"
 "github.com/yuuu/auth\_api/app"
 )
 
 func main() {
 // Create service
 service := goa.New("Secure")
 
 // Mount middleware
 service.Use(middleware.RequestID())
 service.Use(middleware.LogRequest(true))
 service.Use(middleware.ErrorHandler(service, true))
 service.Use(middleware.Recover())
 
 // 追記ここから
 pem, err := ioutil.ReadFile("./jwtkey/jwt.key.pub")
 if err != nil {
 os.Exit(1)
 }
 key, err := jwtgo.ParseRSAPublicKeyFromPEM([]byte(pem))
 if err != nil {
 os.Exit(1)
 }
 jwtHandler := func(h goa.Handler) goa.Handler {
 return func(ctx context.Context, rw http.ResponseWriter, req \*http.Request) error {
 return h(ctx, rw, req)
 }
 }
 app.UseJWTMiddleware(service, jwt.New(jwt.NewSimpleResolver([]jwt.Key{key}), jwtHandler, app.NewJWTSecurity()))
 
 basicAuthHandler := func(h goa.Handler) goa.Handler {
 return func(ctx context.Context, rw http.ResponseWriter, req \*http.Request) error {
 user, pass, ok := req.BasicAuth()
 if !ok || user != "foo" || pass != "bar" {
 errUnauthorized := goa.NewErrorClass("unauthorized", 401)
 return errUnauthorized("missing auth")
 }
 return h(ctx, rw, req)
 }
 }
 app.UseSigninBasicAuthMiddleware(service, basicAuthHandler)
 // 追記ここまで
 
 // Mount "jwt" controller
 c := NewJWTController(service)
 app.MountJWTController(service, c)
 
 // Start service
 if err := service.ListenAndServe(":8080"); err != nil {
 service.LogError("startup", "err", err)
 }
 
 }

POST /jwt/signinを実装

signinの処理を記載します。
読み出した秘密鍵を使ってtokenを生成し、レスポンスのAuthorizationヘッダにセットしています。

func (c \*JWTController) Signin(ctx \*app.SigninJWTContext) error {
 // JWTController\_Signin: start\_implement
 
 // Put your logic here
 b, err := ioutil.ReadFile("./jwtkey/jwt.key")
 if err != nil {
 return fmt.Errorf("read private key file: %s", err) // internal error
 }
 privKey, err := jwtgo.ParseRSAPrivateKeyFromPEM(b)
 if err != nil {
 return fmt.Errorf("failed to parse RSA private key: %s", err) // internal error
 }
 token := jwtgo.New(jwtgo.SigningMethodRS512)
 in3m := time.Now().Add(time.Duration(3) \* time.Minute).Unix()
 token.Claims = jwtgo.MapClaims{
 "iss": "Issuer", // who creates the token and signs it
 "aud": "Audience", // to whom the token is intended to be sent
 "exp": in3m, // time when the token will expire (10 minutes from now)
 "jti": uuid.Must(uuid.NewV4()).String(), // a unique identifier for the token
 "iat": time.Now().Unix(), // when the token was issued/created (now)
 "nbf": 2, // time before which the token is not yet valid (2 minutes ago)
 "sub": "subject", // the subject/principal is whom the token is about
 "scopes": "api:access", // token scope - not a standard claim
 }
 signedToken, err := token.SignedString(privKey)
 if err != nil {
 return fmt.Errorf("failed to sign token: %s", err) // internal error
 }
 ctx.ResponseData.Header().Set("Authorization", "Bearer "+signedToken)
 return ctx.NoContent()
 
 // JWTController\_Signin: end\_implement
 }

GET /jwtを実装

tokenを復号しclaimsの中身をチェックしています。特に問題が無ければOK : trueを返します。

func (c \*JWTController) Secure(ctx \*app.SecureJWTContext) error {
 // JWTController\_Secure: start\_implement
 
 // Put your logic here
 // Retrieve the token claims
 token := jwt.ContextJWT(ctx)
 if token == nil {
 return fmt.Errorf("JWT token is missing from context") // internal error
 }
 claims := token.Claims.(jwtgo.MapClaims)
 
 // Use the claims to authorize
 subject := claims["sub"]
 if subject != "subject" {
 // A real app would probably use an "Unauthorized" response here
 res := &app.GoaExamplesSecuritySuccess{OK: false}
 return ctx.OK(res)
 }
 
 res := &app.GoaExamplesSecuritySuccess{OK: true}
 return ctx.OK(res)
 
 // JWTController\_Secure: end\_implement
 }

動作確認

以下コマンドでAPIサーバを起動します。

$ go run main.go jwt.go 
 2018/11/14 23:08:17 [INFO] mount ctrl=JWT action=Secure route=GET /api/v1/jwt security=jwt
 2018/11/14 23:08:17 [INFO] mount ctrl=JWT action=Signin route=POST /api/v1/jwt/signin security=SigninBasicAuth
 2018/11/14 23:08:17 [INFO] listen transport=http addr=:8080

curlコマンドで/jwt/signinへPOSTしてみます。

$ curl localhost:8080/api/v1/jwt/signin -H "accept: application/json" -H "Authorization:Basic $(echo -n foo:bar | openssl base64)" -X POST -i
 HTTP/1.1 204 No Content
 Authorization: Bearer eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBdWRpZW5jZSIsImV4cCI6MTU0MjM0MDM1NiwiaWF0IjoxNTQyMzQwMTc2LCJpc3MiOiJJc3N1ZXIiLCJqdGkiOiI1NDAxOThiNy01NGEzLTQyMzUtOTlkMC05ZTRjMDE3MDM3YmIiLCJuYmYiOjIsInNjb3BlcyI6ImFwaTphY2Nlc3MiLCJzdWIiOiJzdWJqZWN0In0.dqRAaMwyLpuM0enz-5W0H6BSZNCZmIgl78aSc3aPDArQKb7jU\_JXgzkQXm6kHP1QNJ\_s89uUIOCtRG-B-OUXnhYya07l2AAA\_dzudXeGxM4RSCuyX7xJ7t2N-ZMjMLevgGfPi1CXKMEFjkUx1QSKuzN751goaR7X7NY\_FXz7kZcVI9b2ANXNZb0D6Q77QfhUAPG0BYNYDvaBHlRhAhm8H1wnO6uxspD0LKjM4Mj9ExLh3wuKxgX9OJg6tovjf2RnHByV3xHAkScSpxTh4a\_IYHJKmfduVynubyV1bLJkvmHsrN9qLl3fYb72nNqYFGTV25P\_6vuJSP2WvOwwxYN8Vg
 Date: Fri, 16 Nov 2018 03:49:36 GMT

tokenが取得できました。
このtokenを使って、/jwtをGETします。

$ curl localhost:8080/api/v1/jwt -H "accept: application/json" -H "Authorization: Bearer eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBdWRpZW5jZSIsImV4cCI6MTU0MjM0MDM1NiwiaWF0IjoxNTQyMzQwMTc2LCJpc3MiOiJJc3N1ZXIiLCJqdGkiOiI1NDAxOThiNy01NGEzLTQyMzUtOTlkMC05ZTRjMDE3MDM3YmIiLCJuYmYiOjIsInNjb3BlcyI6ImFwaTphY2Nlc3MiLCJzdWIiOiJzdWJqZWN0In0.dqRAaMwyLpuM0enz-5W0H6BSZNCZmIgl78aSc3aPDArQKb7jU\_JXgzkQXm6kHP1QNJ\_s89uUIOCtRG-B-OUXnhYya07l2AAA\_dzudXeGxM4RSCuyX7xJ7t2N-ZMjMLevgGfPi1CXKMEFjkUx1QSKuzN751goaR7X7NY\_FXz7kZcVI9b2ANXNZb0D6Q77QfhUAPG0BYNYDvaBHlRhAhm8H1wnO6uxspD0LKjM4Mj9ExLh3wuKxgX9OJg6tovjf2RnHByV3xHAkScSpxTh4a\_IYHJKmfduVynubyV1bLJkvmHsrN9qLl3fYb72nNqYFGTV25P\_6vuJSP2WvOwwxYN8Vg"
 {"ok":true}

3分経つとtokenの有効期限が切れて、失敗します。

$ curl localhost:8080/api/v1/jwt -H "accept: application/json" -H "Authorization: Bearer eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBdWRpZW5jZSIsImV4cCI6MTU0MjM0MDM1NiwiaWF0IjoxNTQyMzQwMTc2LCJpc3MiOiJJc3N1ZXIiLCJqdGkiOiI1NDAxOThiNy01NGEzLTQyMzUtOTlkMC05ZTRjMDE3MDM3YmIiLCJuYmYiOjIsInNjb3BlcyI6ImFwaTphY2Nlc3MiLCJzdWIiOiJzdWJqZWN0In0.dqRAaMwyLpuM0enz-5W0H6BSZNCZmIgl78aSc3aPDArQKb7jU\_JXgzkQXm6kHP1QNJ\_s89uUIOCtRG-B-OUXnhYya07l2AAA\_dzudXeGxM4RSCuyX7xJ7t2N-ZMjMLevgGfPi1CXKMEFjkUx1QSKuzN751goaR7X7NY\_FXz7kZcVI9b2ANXNZb0D6Q77QfhUAPG0BYNYDvaBHlRhAhm8H1wnO6uxspD0LKjM4Mj9ExLh3wuKxgX9OJg6tovjf2RnHByV3xHAkScSpxTh4a\_IYHJKmfduVynubyV1bLJkvmHsrN9qLl3fYb72nNqYFGTV25P\_6vuJSP2WvOwwxYN8Vg"
 {"id":"TCklakKK","code":"jwt\_security\_error","status":401,"detail":"JWT validationfailed"}

もちろん、tokenが無くても失敗します。

$ curl localhost:8080/api/v1/jwt -H "accept: application/json"
 {"id":"i2irAndZ","code":"jwt\_security\_error","status":401,"detail":"missing header\"Authorization\""}

まとめ

フレームワークの仕様さえ理解すれば簡単にJWT認証を追加できました。

次はgormaを使ったモデル定義をしていく予定です。

Yuhei Okazaki

Yuhei Okazaki

2018年の年明けに組込み畑からやってきた、2児の父 兼 Webエンジニアです。 mockmockの開発・運用を担当しており、組込みエンジニア時代の経験を活かしてデバイスをプログラミングしたり、簡易的なIoTシステムを作ったりしています。主な開発言語はRuby、時々Go。