1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails × GraphQL でAPI開発|ログイン認証を行う

Last updated at Posted at 2025-04-27

はじめに

この記事では、GraphQL Ruby を使用した Rails アプリケーションでの認証フローの実装方法について解説します 🙋‍♂️
セッションモデルを活用した認証システムで、ユーザーのログイン・ログアウト機能を実装する方法を紹介します。

🔍 セットアップ方法は過去記事をご参考ください

認証フロー

GraphQL を使用した認証フローは以下のようになります。

実装手順

1. CORS, Cookie 設定

事前対応として、フロントエンドからのリクエストを受け付けるようにしておきます。

rubyconfig/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '127.0.0.1:3000'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      headers: :any,
      credentials: true
  end
end

API Mode では Cookie が利用できないので、Cookie が利用できるように設定します。

config/application.rb
module App
  class Application < Rails::Application
    # ...
    config.middleware.use ActionDispatch::Cookies # 追加
  end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::Cookies # 追加
end

2. BCryptのインストール

パスワードのハッシュ化や認証メソッド提供のため、事前に「BCrypt」をインストールしておいてください。

gem 'bcrypt', '~> 3.1.7'
bundle install

3. モデルの準備

ユーザーモデルを作成します。

app/models/user.rb
# == Schema Information
#
# Table name: users
#
#  id              :bigint           not null, primary key
#  email           :string(255)      not null
#  name            :string(255)      not null
#  password_digest :string(255)      not null
#  created_at      :datetime         not null
#  updated_at      :datetime         not null
#
class User < ApplicationRecord
  has_secure_password
  
  has_many :sessions, dependent: :destroy

  def create_session!
    sessions.create!(key: Session.generate_key)
  end

  def current_session
    sessions.last
  end
end

次にログイン状態を管理するセッションモデルを作成します。

app/models/session.rb
# == Schema Information
#
# Table name: sessions
#
#  id         :bigint           not null, primary key
#  key        :string(255)      not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#  user_id    :bigint           not null
#
class Session < ApplicationRecord
  belongs_to :user

  validates :key, presence: true, uniqueness: true

  def self.generate_key
    SecureRandom.hex(20)
  end
end

4. ログインミューテーションの実装

ログイン処理を作成していきます。
user.authenticate でのユーザー認証成功時にセッションを作成し、セッションキーを Cookieに格納します。

app/graphql/mutations/login_mutation.rb
module Mutations
  class LoginMutation < GraphQL::Schema::Mutation
    argument :email, String, required: true
    argument :password, String, required: true

    field :token, String, null: true
    field :user, Types::UserType, null: true

    def resolve(email:, password:)
      user = User.find_by(email:)

      if user && user.authenticate(password)
        user.create_session!
        token = user.current_session.key

        context[:cookies].signed[:sid] = {
          value: token,
          expires: 1.year.from_now,
          httponly: true,
          secure: true,
          same_site: :lax
        }

        {
          token: token,
          user: user,
        }
      else
        raise GraphQL::ExecutionError, "Invalid credentials"
      end
    end
  end
end

5. ログアウトミューテーションの実装

ログアウト処理を作成していきます。
こちらは単純に cookie を削除します。

app/graphql/mutations/logout_mutation.rb
module Mutations
  class LogoutMutation < GraphQL::Schema::Mutation
    field :success, Boolean, null: false

    def resolve
      if context[:cookies].signed[:sid]
        context[:cookies].delete(:sid)

        { success: true }
      else
        { success: false }
      end
    end
  end
end

6. ミューテーションタイプに登録

作成したミューテーションを登録します。

app/graphql/types/mutation_type.rb
module Types
  class MutationType < Types::BaseObject
    field :login, mutation: Mutations::LoginMutation
    field :logout, mutation: Mutations::LogoutMutation
  end
end

7. GraphQL コントローラーの設定

コンテキストで 「Cookie」 と 「ログイン中ユーザー情報」 が参照できるようにします。

app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  def execute
    # ...
    context = {
      cookies: cookies,
      current_user: current_user
    }
    # ...
  end

  private

  def current_user
    token = cookies.signed[:sid]
    return nil unless token.present?

    session = Session.find_by(key: token)
    session&.user
  end
end

8. ユーザータイプの定義

app/graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

    field :posts, [Types::PostType], null: true

    def posts
      object.posts
    end
  end
end

9. 現在のユーザーを取得するクエリ

カレントユーザーを取得するクエリも合わせて作成しておきましょう。

app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    # ...
    
    field :current_user, Types::UserType, null: true

    def current_user
      context[:current_user]
    end
    
    # ...
  end
end

動作確認

以下のクエリでログイン、ログアウト、認証状態の確認ができます。

# ログイン
mutation {
  login(email: "user@example.com", password: "password") {
    token
    user {
      id
      name
      email
    }
  }
}

# 現在のユーザー情報取得
query {
  currentUser {
    id
    name
    email
    posts {
      id
      title
    }
  }
}

# ログアウト
mutation {
  logout {
    success
  }
}

Cookieにも値が保存されていることを確認できました🙌

スクリーンショット 2025-04-27 17.51.58.png

まとめ

GraphQL Rubyを使用したRailsアプリケーションで、Sessionモデルを活用した認証システムを実装する方法を解説しました👌

認可処理についても後ほど別記事にて投稿予定です🙋‍♂️


[補足] Next.js での認証方法

先ほど実装した認証機能を、Next.js アプリケーションで実装する方法を解説します。

フロー

1. GraphQL クエリの準備

まず、現在のユーザー情報を取得する GraphQL クエリを定義します。Apollo Clientを使用して、サーバーから認証情報を取得します。
セットアップ方法については、以下の記事をご参考ください🙏

src/app/graphql/auth/queries.ts
import { gql } from '@apollo/client';

export const CURRENT_USER_QUERY = gql`
  query CurrentUser {
    currentUser {
      id
      email
      name
    }
  }
`;

2. ユーザー取得ロジックの実装

次にユーザー取得関数を作成します。

現在の実装では、middlewareServerComponent の両方のコンテキストで動作するように設計しています。

最適化や改善の余地があれば、ご教示いただきたいです🙏

src/app/(auth)/fetcher.ts
import 'server-only';

import { NextRequest } from "next/server";
import { apolloClient } from "../graphql";
import { CURRENT_USER_QUERY } from "../graphql/auth/queries";
import { User } from "@/app/types";
import { cookies } from "next/headers";

export async function getCurrentUser(request?: NextRequest): Promise<User | null> {
  let cookieHeader: string | null = null;

  // NOTE: call from middleware
  if (request) {
    cookieHeader = request.headers.get('cookie');
  }
  // NOTE: call from Server Component
  else {
      const cookiesObj = await cookies();
      cookieHeader = cookiesObj.toString();
  }

  if (!cookieHeader) {
    return null;
  }

  const { data } = await apolloClient.query({
    query: CURRENT_USER_QUERY,
    context: {
      headers: {
        Cookie: cookieHeader,
      },
    },
  });
  return data.currentUser as User;
}

3. ミドルウェアの設定

特定のルートを認証済みユーザーのみにアクセス制限するミドルウェアを実装します。
以下の例では、未ログイン状態で /account と /dashboard にアクセスすると、TOPページにリダイレクトされます。

src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { getCurrentUser } from './app/(auth)/fetcher';

const protectedRoutes = ['/account', '/dashboard'];

function isProtectedRoute(path: string): boolean {
  return protectedRoutes.some(route => {
    return path === route || path.startsWith(`${route}/`);
  });
}

export async function middleware(request: NextRequest) {
  const path = new URL(request.url).pathname;

  if (isProtectedRoute(path)) {
    const user = await getCurrentUser(request);

    if (!user) {
      return NextResponse.redirect(new URL('/', request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico).*)',
    '/((?!.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'
  ],
};

4. コンポーネントでの認証状態考慮

認証状態に基づいて異なる UI を表示します。

以下は、ユーザーの認証状態に応じて異なるボタンを表示するコンポーネントの一例です。

import { getCurrentUser } from '@/app/(auth)/fetcher'
import Link from 'next/link'

export default async function PublicHeader() {
  const currentUser = await getCurrentUser();

  return (
    <>
      {currentUser ? (
        <Link href="/account">
          <button>マイページ</button>
        </Link>
      ) : (
        <Link href="/signin">
          <button>ログイン</button>
        </Link>
      )}
    </>
  )
}
1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?