sample-graphqlにログイン処理を実装していきます。
schema.graphqlでemailとパスワードをUserに追加し、LoginAPIを叩いたらログイン用のトークンが返ってくるようにする。
type User {
id: ID!
name: String!
+ email: String!
+ password: String!
}
input NewUser {
name: String!
+ email: String!
+ password: String!
}
+input AuthorizationRequest {
+ email: String!
+ password: String!
+}
+
+type AuthorizationResponse {
+ token: String!
+}
type Mutation {
createTodo(input: NewTodo!): Todo!
createUser(input: NewUser!): User!
+ authorization(input: AuthorizationRequest!): AuthorizationResponse! # 認可
}gqlgen generateの実行
gqlgen generateapp/domain/authToken/authToken.goにトークンのドメインモデルを実装する。
package authToken
import (
"context"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
type authToken struct {
userID string
token string
}
const (
// secret は openssl rand -base64 40 コマンドで作成した。TODO:環境変数化
secret = "1ptTwOz/dcTcYVuzDbHS+S7nLCN9eO/LU+iSirrlTPyBkRl3lXbw0A=="
// contextにセットするキー
contextKey = "auth_token"
// userIDKey はユーザーの ID を表す。
userIDKey = "user_id"
// iat と exp は登録済みクレーム名。それぞれの意味は https://tools.ietf.org/html/rfc7519#section-4.1 を参照
iatKey = "iat"
expKey = "exp"
// lifetime は jwt の発行から失効までの期間を表す。
lifetime = 30 * time.Minute
)
// userIDからjwtトークンを発行する
func New(userID string, now time.Time) (*authToken, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
userIDKey: userID,
iatKey: now.Unix(),
expKey: now.Add(lifetime).Unix(),
})
tokenStr, err := token.SignedString([]byte(secret))
if err != nil {
return nil, err
}
return &authToken{
userID: userID,
token: tokenStr,
}, nil
}
// contextにトークンをセットする
func SetToken(parents context.Context, t authToken) context.Context {
return context.WithValue(parents, contextKey, &t)
}
// contextからトークンを取得する
func GetToken(ctx context.Context) (*authToken, error) {
v := ctx.Value(contextKey)
token, ok := v.(*authToken)
if !ok {
return nil, fmt.Errorf("token not found")
}
return token, nil
}
// トークンを検証しauthTokenを生成する
func GenerateAuthByToken(tokenString string) (authToken, error) {
token, err := verifyToken(tokenString)
if err != nil {
return authToken{}, err
}
claims := token.Claims.(jwt.MapClaims)
return authToken{
userID: claims[userIDKey].(string),
token: token.Raw,
}, nil
}
func (a authToken) GetUserID() string {
return a.userID
}
func (a authToken) GetToken() string {
return a.token
}
func verifyToken(tokenString string) (*jwt.Token, error) {
// jwtの検証(改ざん検知、期限切れ)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
return token, nil
}app/middleware/auth.go
package middleware
import (
"net/http"
"github.com/amasok/sample-graphql/app/domain/authToken"
)
// TODO: configでoriginを切り分けられるようにする
func AuthForGraphql(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth == "" {
next.ServeHTTP(w, r)
return
}
bearer := "Bearer "
auth = auth[len(bearer):]
token, err := authToken.GenerateAuthByToken(auth)
if err != nil {
next.ServeHTTP(w, r)
return
}
// ここでトークンをcontextにセットする
ctx := authToken.SetToken(r.Context(), token)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}app/presentation/graphql/directive/auth.goの実装
上記、middlewareでセットしたcontextのトークンが正しくセットされているか確認する。
package directive
import (
"context"
"fmt"
"github.com/99designs/gqlgen/graphql"
"github.com/amasok/sample-graphql/app/domain/authToken"
"github.com/vektah/gqlparser/v2/gqlerror"
)
func Auth(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
tokenData, err := authToken.GetToken(ctx)
if err != nil {
return nil, &gqlerror.Error{
Message: "Access Denied",
}
}
// ログインユーザーIDを見てみる
fmt.Printf("login user_id: %s", tokenData.GetUserID())
return next(ctx)
}cmd/graphql/main.goの実装
上記、middlewareとdirectiveの認証をmain.goに適用する。
@@ -9,6 +9,7 @@ import (
"github.com/99designs/gqlgen/graphql/playground"
"github.com/amasok/sample-graphql/app/middleware"
"github.com/amasok/sample-graphql/app/presentation/graphql"
+ "github.com/amasok/sample-graphql/app/presentation/graphql/directive"
"github.com/amasok/sample-graphql/app/presentation/graphql/generated"
"github.com/gorilla/mux"
)
@@ -23,7 +24,16 @@ func main() {
r := mux.NewRouter()
r.Use(middleware.CORSForGraphql)
- srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graphql.Resolver{}}))
+ r.Use(middleware.AuthForGraphql) // ここでヘッダーにトークンがあれば内容をチェックしてcontextにセットする
+
+ config := generated.Config{
+ Resolvers: &graphql.Resolver{},
+ Directives: generated.DirectiveRoot{
+ Auth: directive.Auth, // @authがついてるディレクティブで認証の承認を行う
+ },
+ }
+
+ srv := handler.NewDefaultServer(generated.NewExecutableSchema(config))
r.Handle("/", playground.Handler("GraphQL playground", "/query"))
r.Handle("/query", srv)ログインしてトークンを取得する

@auth ディレクティブが設定されているCreateTodoで認証が通るか、通らなければエラーが変えるか確認する
↓ヘッダーにログイン時に返ってきたトークンをセットした場合

↓トークンをセットしていない場合

https://github.com/amasok/sample-graphql/pull/2
https://gqlgen.com/recipes/authentication/
https://ichi.pro/jwt-o-shiyoshite-go-graphql-o-ninshosuru-289582363661