0
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?

React Router v7 アプリケーションで middleware を使って統合windows認証を制御してみた

Last updated at Posted at 2025-11-17

概要

前回作成した React Router v7(フレームワークモード)での統合 windows 認証のコンポーネントを使って、middleware と layout で制御してみます。

middleware の機能を使用するには、React Router 7.9 以降が必要です。

参考にしたサイト

構成

app/
├── routes/
│   ├── _auth.tsx (レイアウト)
│   ├── home.tsx (認証成功時に表示される画面)
│   └── error.tsx (認証失敗時に表示される画面)
├── types/
│   └── interface.d.ts (グローバルで使用するインターフェース)
├── utilities/
│   └── windowsAuth.ts (windows認証する)
├── root.tsx
└── routes.ts

middleware の構成

  • root.tsx - rootMiddleware - rootContext
    middleware で windows 認証します
  • _auth.tsx - authMiddleware - authContext
    windows 認証されていなければエラーページを表示、認証されていればユーザの情報を取得します
  • home.tsx - loader
    _auth で取得したユーザ情報を使用してデータを取得します

middleware は並列実行ではなく、親 → 子の順に実行されます。

設定

react-router.config.tsv8_middleware の記述を追加して有効化します。

react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
	ssr: true,
	future: {
		v8_middleware: true,
	},
} satisfies Config;

root.tsx

middleware を使って windows 認証されたユーザを取得します。
前回 loader に記述したものを middleware に移行します。
開発環境でも動かせるように、.envVITE_DEBUG_USER=domain/username を追加するとそのユーザで認証されたことにしています。
context.set(rootContext, { userName: userInfo.user, domain: userInfo.domain });
この部分で、root の middleware で管理する rootContext に、ユーザ名とドメイン名を保存しています。

root.tsx
import {
	createContext,
	isRouteErrorResponse,
	Links,
	Meta,
	Outlet,
	Scripts,
	ScrollRestoration,
} from "react-router";
import * as React from "react";

import type { Route } from "./+types/root";
import "./app.css";

// middleware用のコンテキスト作成
export const rootContext = createContext<AuthenticatedUser>();

/**
 * windows認証ミドルウェア
 * ユーザー名・ドメイン名を取得します
 * サーバー専用として扱いたいので、この関数はexportしないこと
 */
const rootMiddleware: Route.MiddlewareFunction = async ({ request, context }) => {
	try {
		// DEV環境なら固定のユーザーを返す(.envから読み取り)
		if (typeof process !== "undefined" && process.env.NODE_ENV === "development") {
			const [domain, userName] = (process.env.VITE_DEBUG_USER || "unknown").split("\\");
			context.set(rootContext, { userName, domain });
			return;
		}

		// x-iis-windowsauthtoken ヘッダーを取得
		const tokenHeader = request.headers.get("x-iis-windowsauthtoken");
		if (!tokenHeader) {
			console.log("No authentication token found in request headers.");
			context.set(rootContext, { userName: "", domain: "" });
			return;
		}

		// トークンを16進数から数値に変換
		const handle = parseInt(tokenHeader, 16);
		if (isNaN(handle)) {
			console.log("Invalid authentication token format.");
			context.set(rootContext, { userName: "", domain: "" });
			return;
		}

		// Windows認証情報を取得(サーバーサイド専用)
		const { getUserInfoFromToken } = await import("./utilities/windowsAuth");
		const userInfo = getUserInfoFromToken(handle);
		context.set(rootContext, { userName: userInfo.user, domain: userInfo.domain });
	} catch (error) {
		console.error("Error getting auth data from request:", error);
		context.set(rootContext, { userName: "", domain: "" });
	}
};

export const middleware: Route.MiddlewareFunction[] = [rootMiddleware];

// layoutなど...
interface.d.ts
interface AuthenticatedUser {
	userName: string;
	domain: string;
}

interface User {
	id: number;
	name: string;
	permission: number;
}

routes.tsx

_auth.tsx を layout として、認証が必要なルートをその中に入れます。
windows 認証なので基本的には認証失敗はあまりないと思いますが、想定されたドメインのユーザでなかったり、特定のグループを拒否したりする場合はここで弾きます。

routes.tsx
import { type RouteConfig, index, layout, route } from "@react-router/dev/routes";

export default [
	// 認証が必要なルート
	layout("routes/_auth.tsx", [
        index("routes/home.tsx"),
        route("/feature1", "routes/feature1.tsx"),
        route("/feature2", "routes/feature1.tsx"),
        ...
	]),
	// エラーページ(認証不要)
	route("error", "routes/error.tsx"),
] satisfies RouteConfig;

_auth.tsx

root.tsx の middleware で取得したユーザ名を使用し、ユーザの情報を API から取得した感じにしています。取得したデータは authContext に保存します。

_auth.tsx
import { Outlet, redirect, createContext } from "react-router";
import { rootContext } from "~/root";
import type { Route } from "./+types/_auth";
import type { User } from "~/types/api";

// middleware用のコンテキスト作成
export const authContext = createContext<User>();

// ユーザー情報を取得
async function fetchUserInfo(userName: string) {
	// 実際のAPI呼び出しをシミュレート
	await new Promise((resolve) => setTimeout(resolve, 100));
	return { id: 123, name: userName, permission: 755 };
}

// 認証済みチェックミドルウェア
const authMiddleware: Route.MiddlewareFunction = async ({ context }) => {
	const authenticatedUser = context.get(rootContext);
	if (authenticatedUser.userName == "") {
		throw redirect("/error");
	}
	// user情報取得処理
	const user = await fetchUserInfo(authenticatedUser.userName);
    // userについての情報が取得できなかった場合はエラーとする
	if (!user || user.id === 0) {
		throw redirect("/error");
	}
	context.set(authContext, user);
};

export const middleware: Route.MiddlewareFunction[] = [authMiddleware];

export default function AuthLayout() {
	return <Outlet />;
}

home.tsx

_auth で取得したユーザ情報を元に home.tsx の loader でデータを取得します。
画面に対するアクセス権限のチェックもここでやると良さそうです。

home.tsx
import type { Route } from "./+types/home";
import { authContext } from "~/routes/_auth";

// サーバサイドloader
export async function loader({ context }: Route.LoaderArgs) {
    // _authのmiddlewareで取得したユーザ情報を取得
	const user: User = context.get(authContext)!;
    // 何かしらのフェッチ処理
	const retrieveData = await fetchExampleData(user.userName);
	return { retrieveData, user };
}

// ユーザー名に対応するデータを取得
async function fetchExampleData(userName: string) {
	// 実際のAPI呼び出しをシミュレート
	await new Promise((resolve) => setTimeout(resolve, 100));
	return { example: 123 };
}

export default function Home({ loaderData }: Route.ComponentProps) {
	return (
		<div className="flex flex-1 overflow-hidden">
			<SidebarMenu />
			<div className="flex-1 overflow-y-auto p-4">
				<div className="mt-4 p-4 bg-gray-100 rounded">
					{loaderData.user && (
						<div className="mt-2 p-2 bg-blue-100 rounded">
							<div>User Data:</div>
							<div>ID: {loaderData.user.id}</div>
							<div>Name: {loaderData.user.name}</div>
							<div>ExampleValue: {loaderData.retrieveData.example}</div>
						</div>
					)}
				</div>
			</div>
		</div>
	);
}
0
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
0
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?