goaを使っていてちょっと困ったことがありました。
goaの認証はこんな感じで設定しますよね
var _ = Resource("basic", func() {
Description("This resource uses basic auth to secure its endpoints")
DefaultMedia(SuccessMedia)
Security(BasicAuth)
Action("secure", func() {
Description("This action is secure with the basic_auth scheme")
Routing(GET("/basic"))
Response(OK)
Response(Unauthorized)
})
Action("unsecure", func() {
Description("This action does not require auth")
Routing(GET("/basic/unsecure"))
NoSecurity()
Response(OK)
})
})
でも、 Security(BasicAuth)
と NoSecurity()
で、
認証あり/なしどちらかのルートしか設定できないように見えます。
例えば認証あり/なしで共通のルートを作りたいと考えました
(認証ありの場合は追加の情報を足して返すようにしたい等)
slackで相談したら解決方法を教えていただいたので実装を纏めてみたいと思います。
結論から言うと、実装は以下においています
https://github.com/m0a-mystudy/goa-optional-token
basic認証の場合
以下のようにgoaで設計します。
package design
import (
. "github.com/goadesign/goa/design"
. "github.com/goadesign/goa/design/apidsl"
)
// BasicAuth defines a security scheme using basic authentication.
var BasicAuth = BasicAuthSecurity("basic_auth")
// OptionalBasicAuth defines is all user access
var OptionalBasicAuth = BasicAuthSecurity("optional_basic_auth")
var _ = Resource("basic", func() {
Description("This resource uses basic auth to secure its endpoints")
DefaultMedia(SuccessMedia)
Security(BasicAuth)
Action("secure", func() {
Description("This action is secure with the basic_auth scheme")
Routing(GET("/basic"))
Response(OK)
Response(Unauthorized)
})
Action("optional", func() {
Description("This action is optional secure with the basic_auth scheme")
Security(OptionalBasicAuth)
Routing(GET("/basic/optional"))
Response(OK)
})
Action("unsecure", func() {
Description("This action does not require auth")
Routing(GET("/basic/unsecure"))
NoSecurity()
Response(OK)
})
})
basic/optional
のルートを追加しており
こちらについて認証あり/なし 関係無しでアクセスできるようにします。
ただし認証ありならtokenをcontextに登録し後から取得できるようにしておきます。
まず認証有り無し関係なしでも処理が継続できるようにするためのOptionalBasicAuth
を追加します。
OptionalBasicAuth
用のmiddlewareを書きます。
type OptionalBasicAuth struct {
User string
Pass string
}
type optionalBasicAuthKeyType int
const (
optionalBasicAuthKey optionalBasicAuthKeyType = iota + 1
)
func NewOptinalBasicAuthMiddleware() goa.Middleware {
return func(h goa.Handler) goa.Handler {
return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
// Retrieve and log basic auth info
user, pass, ok := req.BasicAuth()
// A real app would do something more interesting here
if !ok {
goa.LogInfo(ctx, "failed basic auth")
return h(ctx, rw, req) // 処理継続するためにnextHandlerを呼び出す。
}
// Proceed
goa.LogInfo(ctx, "basic", "user", user, "pass", pass)
ctx = context.WithValue(ctx, optionalBasicAuthKey, &OptionalBasicAuth{
User: user,
Pass: pass,
})
return h(ctx, rw, req)
}
}
}
// contextからbasic認証時のuser passを取得する
func ContextOptionalBasicAuth(ctx context.Context) *OptionalBasicAuth {
if v := ctx.Value(optionalBasicAuthKey); v != nil {
return v.(*OptionalBasicAuth)
}
return nil
}
上記のようにbasic認証失敗時でも処理を継続するように変更します
basic認証成功時はcontextに認証時の情報を保存しておきます。
実際に使う側では以下のようにmiddlewareの呼び出しを追加するのを忘れないようにして下さい
app.UseBasicAuthMiddleware(service, NewBasicAuthMiddleware())
app.UseOptionalBasicAuthMiddleware(service, NewOptinalBasicAuthMiddleware()) // <-- 追加
Action内の実装は以下のとおりです
// Optional runs the secure action.
func (c *BasicController) Optional(ctx *app.OptionalBasicContext) error {
fmt.Println("in Optional", ContextOptionalBasicAuth(ctx))
res := &app.Success{OK: true}
return ctx.OK(res)
}
上記が追加されたActionで
ContextOptionalBasicAuth
関数 にて contextからtokenが取得できます。
jwt認証の場合
基本的には同じ対応です。
先ず追加のルート設計から
var JWT = JWTSecurity("jwt", func() {
Header("Authorization")
Scope("api:access", "API access") // Define "api:access" scope
})
// 追加
var OptionalJWT = JWTSecurity("optional_jwt", func() {
Header("Authorization")
Scope("api:access", "API access") // Define "api:access" scope
})
var SigninBasicAuth = BasicAuthSecurity("SigninBasicAuth")
// Resource jwt uses the JWTSecurity security scheme.
var _ = Resource("jwt", func() {
Description("This resource uses JWT to secure its endpoints")
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() {
Description("Creates a valid JWT")
Security(SigninBasicAuth)
Routing(POST("/jwt/signin"))
Response(NoContent, func() {
Headers(func() {
Header("Authorization", String, "Generated JWT")
})
})
Response(Unauthorized)
})
Action("secure", func() {
Description("This action is secured with the jwt scheme")
Routing(GET("/jwt"))
Params(func() {
Param("fail", Boolean, "Force auth failure via JWT validation middleware")
})
Response(OK)
Response(Unauthorized)
})
// 追加
Action("optional", func() {
Description("This action is secured with the jwt scheme")
Routing(GET("/jwt/optional"))
Security(OptionalJWT, func() {
Scope("api:access")
})
Response(OK)
})
Action("unsecure", func() {
Description("This action does not require auth")
Routing(GET("/jwt/unsecure"))
NoSecurity() // Override the need for auth
Response(OK)
})
})
jwt/optional
のルートを追加しており
こちらについて認証あり/なし 関係無しでアクセスできるようにします。
middlewareが若干面倒くさかったです。
// NewOptionalJWTMiddleware creates a middleware that checks for the presence of a JWT Authorization header
func NewOptionalJWTMiddleware() (goa.Middleware, error) {
keys, err := LoadJWTPublicKeys()
if err != nil {
return nil, err
}
jwtMiddleware := jwt.New(jwt.NewSimpleResolver(keys), ForceFail(), app.NewJWTSecurity())
return func(nextHandler goa.Handler) goa.Handler {
return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
err := jwtMiddleware(nextHandler)(ctx, rw, req)
if err != nil {
// err が起きた場合 nextHandlerは呼ばれないが、今回は処理を継続する
return nextHandler(ctx, rw, req)
}
return nil
}
}, nil
}
jwtMiddleware
の部分が元々のmiddlewareの部分ですが
error
の場合はnextHandler
の呼び出しを中止するのでerror
の場合でも
強制的にnextHandler
を呼んで処理を継続させる処理に変更しました。
使う側でのmiddleware呼び出しも忘れずに行います。
jwtMiddleware, _ := NewJWTMiddleware()
optionalJwtMiddleware, _ := NewOptionalJWTMiddleware() // <-- 追加
app.UseJWTMiddleware(service, jwtMiddleware)
app.UseOptionalJWTMiddleware(service, optionalJwtMiddleware) // <-- 追加
Action内の実装は以下のとおりです
// Optional runs the optional action.
func (c *JWTController) Optional(ctx *app.OptionalJWTContext) error {
fmt.Println("in Optional")
token := jwt.ContextJWT(ctx)
if token != nil {
claims := token.Claims.(jwtgo.MapClaims)
fmt.Println(claims)
} else {
fmt.Println("token is nothing.")
}
res := &app.Success{OK: true}
return ctx.OK(res)
}
jwt.ContextJWT
関数にてcontextからtokenの取得ができます。