LoginSignup
2
4

More than 5 years have passed since last update.

認証有/無 両方で動くactionを作る

Last updated at Posted at 2017-07-20

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の取得ができます。

2
4
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
4