Goa(v2)でjwt認証
GoaでJWT認証できるAPIを作ろうとこちらのリンクを参考にしたのですがそれでもハマりポイントがあったのでメモ
goaで作ったAPIサーバにJWT認証を追加する | Fusic Tech Blog
jwtキーを発行するURIにBasic認証をかけ、認証が成功するとキーを発行するようにしています。
design.go
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = API("Secure", func() { Title("Secure API") Description("Secure API use JWT") BasePath("/api") Scheme("http") Host("localhost:8080") }) var BasicAuth = BasicAuthSecurity("BasicAuth", func() { Description("Use client ID and client secret to authenticate") }) var JWT = JWTSecurity("jwt", func() { Header("Authorization") Scope("api:access", "API access") }) var _ = Resource("jwt", func() { // Resources group related API endpoints DefaultMedia(SuccessMedia) // services. Security(JWT, func() { Scope("api:access") }) Action("signin", func() { // Actions define a single API endpoint together // ←ここでBasic認証。成功するとJWTトークンが取れる Description("Get JWT Token") // with its path, parameters (both path Security(BasicAuth) Routing(GET("/jwt/signin")) // parameters and querystring values) and payload Response(NoContent, func() { Headers(func() { Header("Authorizatiton", String, "Generated JWT") }) }) Response(Unauthorized) // of HTTP responses. }) Action("secure", func() { // ←取得したJWTが必要なAPI Routing(GET("/jwt")) Response(OK) Response(Unauthorized) }) }) var SuccessMedia = MediaType("application/vnd.goa.jwt.test.success", func() { Description("A station of mine") Attributes(func() { // Attributes define the media type shape. Attribute("ok", Boolean, "Always true") Required("ok") }) View("default", func() { // View defines a rendering of the media type. Attribute("ok") // Media types may have multiple views and must }) })
コード生成
goagen bootstrap -d github.com/hryktrd/jwtTest/design
実装
- main.goにmiddleware追加
//go:generate goagen bootstrap -d github.com/hryktrd/jwtTest/design package main import ( "context" "fmt" "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/hryktrd/jwtTest/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()) // ここから追加 // JWTキー用追加コード pem, err := ioutil.ReadFile("./jwtkey/jwt.key.pub.pkcs8") if err != nil { fmt.Printf("%s", err) os.Exit(1) } key, err := jwtgo.ParseRSAPublicKeyFromPEM([]byte(pem)) if err != nil { fmt.Printf("%s", err) 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.UseBasicAuthMiddleware(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) } }
- JWTコントローラ実装 jwt.goにサインイン後JWTを返すところとJWTを読み込む機能を実装
package main import ( "fmt" "io/ioutil" "time" "github.com/hryktrd/jwtTest/app" jwtgo "github.com/dgrijalva/jwt-go" "github.com/goadesign/goa" "github.com/goadesign/goa/middleware/security/jwt" "github.com/gofrs/uuid" ) // JWTController implements the jwt resource. type JWTController struct { *goa.Controller } // NewJWTController creates a jwt controller. func NewJWTController(service *goa.Service) *JWTController { return &JWTController{Controller: service.NewController("JWTController")} } // JWTを読み込む実装 // Secure runs the secure action. func (c *JWTController) Secure(ctx *app.SecureJWTContext) error { // JWTController_Secure: start_implement // 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.GoaJWTTestSuccess{OK: false} return ctx.OK(res) } res := &app.GoaJWTTestSuccess{OK: true} return ctx.OK(res) // JWTController_Secure: end_implement } // サインインしてJWTを返す実装 // Signin runs the signin action. func (c *JWTController) Signin(ctx *app.SigninJWTContext) error { // JWTController_Signin: start_implement 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() return nil // JWTController_Signin: end_implement }
JWT生成用鍵準備
$ mkdir jwtkey $ ssh-keygen -t rsa -b 4096 -f jwtkey/jwt.key
公開鍵はPKCS8化しないといけないので変換
$ ssh-keygen -f .\jwtkey\jwt.key -e -m pkcs8 > .\jwtkey\jwt.key.pkcs8
↑このファイルがWindowsでやるとUTF16 LEになってしまったのでVSCodeでUTF8にして保存しなおしました。
実行
$ go run .\main.go .\jwt.go
localhost:8080/jwt/siginin にfoo/bar のBasic認証でアクセスするとBearerトークンが返され、そのトークンで localhost:8080/jwt にアクセスすると200が返ってきます。