Yuhei Okazaki
Table of Contents
参考資料
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を使ったモデル定義をしていく予定です。