Closed9

GoのGraphqlサーバーにAuth0を導入する

tawachantawachan

ログイン機構などがない状態のGraphqlのサーバーにAuth0を導入する作業中のメモ

tawachantawachan

audが合わないって怒られたけど、フロント側でログインするときにaudience指定してなかったのでAUTH0_AUDIENCE入れたら普通に行けた

nextjsなのにSPAを見てたからややこしかったのかもしれない

REGULAR APPでフレームワークをNext.jsを選べばちゃんとインストラクション出てきた(こっちではaudienceの指定がある)

https://github.com/auth0-samples/auth0-nextjs-samples/tree/main/Sample-01

tawachantawachan

とりあえずGraphqlをハンドルしているエンドポイントにmiddleware仕込んだけど、次のことがまだ残っている

  • 特定のquery/mutationだけ認証する(現状、ユーザー作成時など、認証しないquery/mutationが素通りできない)
  • 認証した該当ユーザーの情報をコンテクストに含める(?)
tawachantawachan

結局、graphqlに入る前に

  • Authorizationヘッダーを見て、アクセストークンの認証
  • subをcontextに入れる

までやるようにした

func newAuth0Middleware() *jwtmiddleware.JWTMiddleware {
	issuerURL, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/")
	if err != nil {
		log.Fatalf("Failed to parse the issuer url: %v", err)
	}

	provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)

	jwtValidator, err := validator.New(
		provider.KeyFunc,
		validator.RS256,
		issuerURL.String(),
		[]string{os.Getenv("AUTH0_AUDIENCE")},
		validator.WithCustomClaims(
			func() validator.CustomClaims {
				return &CustomClaims{}
			},
		),
		validator.WithAllowedClockSkew(time.Minute),
	)
	if err != nil {
		log.Fatalf("Failed to create JWT validator: %v", err)
	}

	return jwtmiddleware.New(
		jwtValidator.ValidateToken,
		jwtmiddleware.WithCredentialsOptional(true),
	)
}

あくまでトークンがあればそれが有効であるかをチェックするにとどめている

認可はGraphqlの中でやる想定なので、トークンがない場合は素通りさせる

そのために以下のオプションを足している

jwtmiddleware.WithCredentialsOptional(true),

subをcontextに詰めるのはこれ

func newAuthExtractor() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			claims, ok := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
			if !ok {
				log.Println("invalid claims")
				next.ServeHTTP(w, r)
				return
			}

			sub := claims.RegisteredClaims.Subject
			ctx := context.WithValue(r.Context(), AUTH0_SUBJECT_KEY, sub)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

ここも、そもそもトークンがない場合もあるのでエラーにはしないで次に流している

tawachantawachan

あとはGraphqlでDirectiveを設定

directive @isAuthenticated on FIELD_DEFINITION

必要なQueryにつける

extend type Query {
  user(id: ID!): User @isAuthenticated
  currentUser: User @isAuthenticated
}

そして必要なロジックをDirectiveに実装すればよかった

func NewDirectiveRoot(services *service.Services) graph.DirectiveRoot {
	isAuthenticated := NewIsAuthenticated(services.User)
	return graph.DirectiveRoot{
		IsAuthenticated: isAuthenticated,
	}
}

func NewIsAuthenticated(userService service.UserService) func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
	return func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
		sub := auth0.GetAuth0Subject(ctx)
		_, err = userService.GetUserBySub(ctx, sub)
		if err != nil {
			log.Printf("err: %v", err)
			return nil, errors.New("unauthorized")
		}
		return next(ctx)
	}
}
srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: resolver.NewResolver(services), Directives: directive.NewDirectiveRoot(services)}))
このスクラップは2023/10/20にクローズされました