Blogs

Apollo Graphql サーバーへの認証の統合:JWT 認証

February 11, 2024

私たちはいくつかのAPIに精通しており、各APIには次のような独自の認証方法があることに気づいたかもしれません

  1. JSON ウェブトークン (JWT)
  2. OAuth 2.0
  3. 基本認証
  4. ベアラートークン認証
  5. API キー
  6. セッションベース認証

これらの認証方法にはさまざまなレベルのセキュリティと複雑さがあり、アプリケーションの要件、セキュリティ上の考慮事項、ユーザーエクスペリエンスなどの要素によって選択方法が異なります。

この記事では、graphql API に JWT 認証を実装します。データベースについては、graphql サーバーに MongoDB と Apollo サーバーを使用します。

JWT を理解する

JWT は認証に広く使われている方法です。これには、エンコードされたユーザー情報を含むトークンを生成し、クライアントとサーバーの間で後続のリクエスト用に渡す方法が含まれます。

JSON Web トークン (JWT) には、ヘッダー、ペイロード、署名という 3 つの主要コンポーネントがあります。それぞれについて簡単に説明します。

  1. ヘッダー: ヘッダーは通常、トークンタイプ (」) の 2 つの部分で構成されます。タイプ「) と署名の作成に使用されたハッシュアルゴリズム (「alg」)。
  2. ペイロード: ペイロードには、エンティティ (通常はユーザー) に関するステートメントと追加データであるクレームが含まれています。
  3. 署名: 署名は、エンコードされたヘッダー、エンコードされたペイロード、および秘密鍵を組み合わせて作成されます。

JSON Web トークン (JWT) にはいくつかの利点があり、Web アプリケーションでの認証や情報交換によく使われています。

  1. ステートレス認証: JWTはステートレスです。つまり、サーバーはセッションデータを保存する必要はありません。これにより、サーバーのオーバーヘッドが軽減され、JWT はアプリケーションのスケーリングに最適です。
  2. 強化されたセキュリティ: JWTはデジタル署名が可能で、オプションで暗号化できるため、送信中のデータの整合性と機密性が保証されます。
  3. コンパクトで効率的: JWT はコンパクトで、HTTP ヘッダーまたは URL パラメーターとして簡単に送信できます。これにより、特に複数のサービスが頻繁に通信するマイクロサービスアーキテクチャでは、ネットワークのオーバーヘッドが軽減され、パフォーマンスが向上します。
  4. クロスドメイン互換性: JWTは、さまざまなドメインやプラットフォームで使用できます。
  5. カスタマイズ可能なペイロード: JWT ペイロードにはカスタムクレームとメタデータを含めることができるため、ユーザーの役割、権限、および追加情報を柔軟に定義できます。
  6. スケーラビリティ: サーバーはセッション状態を維持する必要がないため、新しいインスタンスを追加して、既存のユーザーのセッションに影響を与えずに増加した負荷を処理できます。

アポロ GraphQL サーバーのセットアップ

こちらがGitHubのリンクです https://github.com/icon-gaurav/mastering-graphql-with-nodejs/tree/jwt-authentication
これを複製して、私の説明に従うことができます。

JWT 認証の実装

ユーザーが電子メール、ユーザー名、パスワードを持ち、電子メールとパスワードを使用してユーザーを認証し、ユーザーに対応するJWTトークンを生成する簡単な認証を行います。その後、この JWT トークンを使用して API を認証します。

実装を段階的に行いましょう

JWT パッケージのインストール

使用します bcrypt ライブラリを使用してパスワードを暗号化し、ハッシュされたパスワードとログインプロセスでユーザーが入力したパスワードを比較します

npm install --save bcrypt jsonwebtoken

ユーザースキーマの更新

ユーザースキーマを定義し、そのスキーマを MongoDB と同期する方法はすでにわかっています。もう一度やり直したい場合は、こちらの記事へのリンクをご覧ください [MongoDB を使用したグラフィカル API]

// mongodb schema for user object
const userSchema =  new Schema({
    username:String,
    email:String,
    password:String
});

// defining user model
const User = mongoose.model("User", userSchema);

ユーザー登録

Register user mutationは、データベースに新しいユーザーを作成し、プレーンテキストのパスワードを、以下を使用してハッシュ文字列に変換します bcrypt

// register user mutation
registerUser: async (_parent: any, args: any) => {
            const {email, password, username} = args;
            // we are storing hashed password to the database
            const newUser = new User(
                {
                    email,
                    password: bcrypt.hashSync(password, bcrypt.genSaltSync(10)),
                    username: username ?? email
                })
            return await newUser.save();
        }

JWT トークンを生成

を生成するログインミューテーションを実装する jwt ユーザーから提供された認証情報に基づくユーザー用

// login mutation
login: async (_parent: any, args: any, _context: any) => {
            const {email, password} = args;
            const requestedUser = await User.findOne({email: email});
            /*
                we are using bcrypt to compare 2 passwords as we stored hashed password and not the plain text 
                for security reasons
            */
            if (requestedUser && bcrypt.compareSync(password, requestedUser?.password as string)) {
                // user has provided correct email and password
                // generate the signed jwt token
                const token = jwt.sign(requestedUser, "myprivatekey", {expiresIn: '2h'})

                // return the auth payload
                return {
                    token,
                    user: requestedUser
                }
            }else{
                return new Error('Email or password is incorrect!')
            }
        }

ログインミューテーションへの応答として JWT トークンがあります。この JWT トークンを使用すると、E メールとパスワードを何度も入力しなくても API を認証できます。

の JWT トークンの検証 jwt.io

JWT によるリゾルバーの保護

リゾルバーにユーザー情報があるかどうかを確認するチェックポイントを作成して、リゾルバーを保護します。「はい」の場合は認証され、そうでない場合は認証エラーが発生します。

// securing user related post resolver
posts: async (_parent: any, _args: any, context: any) => {
            // fetch the user from the context
            const {user} = context;
            if (user) {
                return Post.find();
            } else {
                return new Error("Unauthenticated!")
            }
        },

リゾルバーでユーザー情報をチェックしているので、どうにかして渡さなければなりません。そこで、コンテキストを使用してユーザーをすべてのリゾルバーに渡し、JWT トークンが有効かどうかを検証する JWT ミドルウェアを使用します。

JWT ミドルウェア

このミドルウェアの主な機能は、 jwt 有効かどうか。JWT が有効な場合はペイロードを返し、それ以外の場合は null を返します。

// jwt validation check middleware
const jwtValidationMiddleware = (token: string) => {
    if (token) {
        return jwt.verify(token?.split(' ')?.[1], "myprivatekey")
    }
}

これで JWT ペイロードができました。次のステップは、ユーザー情報をリゾルバーに渡すことです。

コンテキスト更新

JWT ペイロード情報、つまりユーザー情報をすべてのリゾルバーに渡して、ユーザーの認証を確認します。

// context code
startStandaloneServer(server, {
    context: async ({req, res}) => ({
        user: jwtValidationMiddleware(req?.headers?.authorization as string),
    }),
    listen: {port: 4000}
})
    .then(({url}: any) => {
        console.log(`🚀 Server listening at: ${url}`);

        // connect the mongodb database
        // Database url from atlas cloud
        const DATABASE_URL: string = `mongodb+srv://gauravbytes:Zah5jnclaMXbzANl@gauravbytes.buvimdx.mongodb.net/?retryWrites=true&w=majority`
        mongoose.connect(DATABASE_URL)
            .then(() => {
                console.log('Database connected successfully')
            })
            .catch((e: any) => {
                console.log('Error connecting : ', e?.message)
            })
    });

テスト

ステップ 1: ユーザーの投稿を何も取得せずに取得してみる jwt トークンが渡されました。エラーが発生するはずです

ステップ 2: ログインミューテーションを使用して新しい JWT トークンを生成する

ステップ3: トークンを使用して、このトークンをBearerヘッダーとして渡し、投稿を取得してみます。今度は投稿のリストが表示されます。

おめでとうございます。Graphql APIにJWT認証を正常に実装し、動作しています。