はじめに
この記事では、GraphQL Ruby を使用した Rails アプリケーションでの認証フローの実装方法について解説します 🙋♂️
セッションモデルを活用した認証システムで、ユーザーのログイン・ログアウト機能を実装する方法を紹介します。
🔍 セットアップ方法は過去記事をご参考ください
認証フロー
GraphQL を使用した認証フローは以下のようになります。
実装手順
1. CORS, Cookie 設定
事前対応として、フロントエンドからのリクエストを受け付けるようにしておきます。
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
が利用できるように設定します。
module App
class Application < Rails::Application
# ...
config.middleware.use ActionDispatch::Cookies # 追加
end
end
class ApplicationController < ActionController::API
include ActionController::Cookies # 追加
end
2. BCryptのインストール
パスワードのハッシュ化や認証メソッド提供のため、事前に「BCrypt」をインストールしておいてください。
gem 'bcrypt', '~> 3.1.7'
bundle install
3. モデルの準備
ユーザーモデルを作成します。
# == 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
次にログイン状態を管理するセッションモデルを作成します。
# == 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
に格納します。
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
を削除します。
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. ミューテーションタイプに登録
作成したミューテーションを登録します。
module Types
class MutationType < Types::BaseObject
field :login, mutation: Mutations::LoginMutation
field :logout, mutation: Mutations::LogoutMutation
end
end
7. GraphQL コントローラーの設定
コンテキストで 「Cookie」 と 「ログイン中ユーザー情報」 が参照できるようにします。
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. ユーザータイプの定義
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. 現在のユーザーを取得するクエリ
カレントユーザーを取得するクエリも合わせて作成しておきましょう。
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にも値が保存されていることを確認できました🙌
まとめ
GraphQL Rubyを使用したRailsアプリケーションで、Sessionモデルを活用した認証システムを実装する方法を解説しました👌
認可処理についても後ほど別記事にて投稿予定です🙋♂️
[補足] Next.js での認証方法
先ほど実装した認証機能を、Next.js アプリケーションで実装する方法を解説します。
フロー
1. GraphQL クエリの準備
まず、現在のユーザー情報を取得する GraphQL
クエリを定義します。Apollo Client
を使用して、サーバーから認証情報を取得します。
セットアップ方法については、以下の記事をご参考ください🙏
import { gql } from '@apollo/client';
export const CURRENT_USER_QUERY = gql`
query CurrentUser {
currentUser {
id
email
name
}
}
`;
2. ユーザー取得ロジックの実装
次にユーザー取得関数を作成します。
現在の実装では、middleware
と ServerComponent
の両方のコンテキストで動作するように設計しています。
最適化や改善の余地があれば、ご教示いただきたいです🙏
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ページにリダイレクトされます。
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>
)}
</>
)
}