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 アプリケーションで 統合windows認証してみた

Last updated at Posted at 2025-11-09

概要

でホストしたIISの環境で、統合windows認証してみました。
React Rouver v7でやってますが、Next.js でも同じようにできるはず。

参考にしたサイト

IIS の設定

サイトの「認証」で「Windows 認証」を有効、「匿名認証」を無効にします。
image.png

web.config の httpPlatform のプロパティに
forwardWindowsAuthToken="true"
を追加します。これにより、HTTPリクエストヘッダーに x-iis-windowsauthtoken が追加されます。

web.config
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <handlers>
            <add name="httpPlatformHandler"
              path="*"
              verb="*"
              modules="httpPlatformHandler"
              resourceType="Unspecified"
            />
        </handlers>
        <httpPlatform
          forwardWindowsAuthToken="true"
          processPath="C:\Programs\nodejs\node.exe"
          arguments="C:\projects\rr\node_modules\@react-router\serve\dist\cli.js C:\projects\rr\build\server\index.js"
          stdoutLogEnabled="true"
          stdoutLogFile="C:\projects\rr\logs\stdout"
        >
            <environmentVariables>
                <environmentVariable name="PORT" value="%HTTP_PLATFORM_PORT%" />
                <environmentVariable name="NODE_ENV" value="production" />
            </environmentVariables>
        </httpPlatform>
    </system.webServer>
</configuration>

Node.js側の処理

koffi のインストール

Windows APIを呼ぶ必要があるため、koffi のインストールが必要です。

npm install koffi
または
pnpm install koffi
pnpm approve-builds

IISで認証されたユーザ名をkoffiで取得する

x-iis-windowsauthtoken で受け取れるのは Windows API に渡すハンドルだけなので、そのハンドルを元にユーザ名を取得する処理を追加します。
参考サイトに出ていたのを AI(Cloud Sonnet 4.5) で TypeScriptにしたものです。console.logはログファイルに記録されるので、適宜削除してください。

utilities/windows-auth.ts
import koffi from 'koffi';

// Define Win32 constants
const TokenUser = 1; // TOKEN_INFORMATION_CLASS value for TokenUser

// Define Win32 API structures and functions
const kernel32 = koffi.load('kernel32.dll');
const advapi32 = koffi.load('advapi32.dll');

// Define the CloseHandle function according to Microsoft docs
// BOOL CloseHandle(HANDLE hObject);
const closeHandleFn = kernel32.func('bool CloseHandle(void* hObject)');

// Define the GetTokenInformation function according to Microsoft docs
// BOOL GetTokenInformation(
//   HANDLE                  TokenHandle,
//   TOKEN_INFORMATION_CLASS TokenInformationClass,
//   LPVOID                  TokenInformation,
//   DWORD                   TokenInformationLength,
//   PDWORD                  ReturnLength
// );
const getTokenInformationFn = advapi32.func(
	'bool GetTokenInformation(void* TokenHandle, int TokenInformationClass, void* TokenInformation, uint32 TokenInformationLength, void* ReturnLength)'
);

// Define the LookupAccountSidW function according to Microsoft docs
// BOOL LookupAccountSidW(
//   LPCWSTR       lpSystemName,
//   PSID          Sid,
//   LPWSTR        Name,
//   LPDWORD       cchName,
//   LPWSTR        ReferencedDomainName,
//   LPDWORD       cchReferencedDomainName,
//   PSID_NAME_USE peUse
// );
const lookupAccountSidWFn = advapi32.func(
	'bool LookupAccountSidW(void* lpSystemName, void* Sid, void* Name, void* cchName, void* ReferencedDomainName, void* cchReferencedDomainName, void* peUse)'
);

// Add a function to get the last Windows error
function getLastError(): number {
	try {
		const getLastErrorFn = kernel32.func('uint32 GetLastError()');
		return getLastErrorFn();
	} catch (error) {
		console.error('Error getting last error code:', error);
		return -1;
	}
}

export interface UserInfo {
	user: string;
	domain: string;
}

/**
 * Windows認証トークンからユーザー情報を取得する
 * @param tokenHandle - x-iis-windowsauthtoken ヘッダーから取得したトークンハンドル(16進数)
 * @returns ユーザー名とドメイン名を含むオブジェクト
 */
export function getUserInfoFromToken(tokenHandle: number): UserInfo {
	try {
		// Allocate buffers for token information
		const buflen = 256; // Increase buffer size
		const tokenInfo = Buffer.alloc(buflen);
		const returnLength = Buffer.alloc(4); // DWORD size

		console.log(`Getting token information for handle: ${tokenHandle}`);

		// Get token information
		const tokenInfoResult = getTokenInformationFn(
			tokenHandle,
			TokenUser,
			tokenInfo,
			buflen,
			returnLength
		);

		if (!tokenInfoResult) {
			const error = new Error('GetTokenInformation failed');
			console.error(error.message);
			throw error;
		}

		const returnedLength = returnLength.readUInt32LE(0);
		console.log(`Token information retrieved. Length: ${returnedLength} bytes`);

		// The TOKEN_USER structure has a SID pointer at offset 0
		// We need to read this pointer to get the actual SID
		const sidPointer = tokenInfo.readBigUInt64LE(0);
		console.log(`SID pointer extracted: ${sidPointer}`);

		if (sidPointer === 0n) {
			console.error('SID pointer is NULL');
			throw new Error('Invalid SID pointer');
		}

		// Prepare buffers for LookupAccountSidW
		const nameBufferSize = 256; // Increase buffer size
		const domainBufferSize = 256; // Increase buffer size

		const nameBuffer = Buffer.alloc(nameBufferSize * 2); // UTF-16LE (2 bytes per char)
		const domainBuffer = Buffer.alloc(domainBufferSize * 2); // UTF-16LE (2 bytes per char)

		const nameLength = Buffer.alloc(4); // DWORD size
		nameLength.writeUInt32LE(nameBufferSize, 0);

		const domainLength = Buffer.alloc(4); // DWORD size
		domainLength.writeUInt32LE(domainBufferSize, 0);

		const sidUseType = Buffer.alloc(4); // SID_NAME_USE size

		console.log('Calling LookupAccountSidW...');

		// Look up the account SID - use the SID pointer directly
		const lookupResult = lookupAccountSidWFn(
			null, // lpSystemName (NULL for local system)
			BigInt(sidPointer), // Convert to BigInt for consistency
			nameBuffer,
			nameLength,
			domainBuffer,
			domainLength,
			sidUseType
		);

		if (!lookupResult) {
			// Get the Windows error code
			const lastError = getLastError();
			console.error(`LookupAccountSidW failed with error code: ${lastError}`);
			throw new Error(`LookupAccountSidW failed with error code: ${lastError}`);
		}

		// Convert buffers to strings, trim null characters
		const nameSize = nameLength.readUInt32LE(0);
		const domainSize = domainLength.readUInt32LE(0);

		const userName = nameBuffer.toString('utf16le', 0, nameSize * 2).replace(/\0+$/, '');
		const domainName = domainBuffer.toString('utf16le', 0, domainSize * 2).replace(/\0+$/, '');

		console.log(`User info retrieved - Name: ${userName}, Domain: ${domainName}`);

		// Close the handle
		closeHandleFn(tokenHandle);

		return {
			user: userName,
			domain: domainName
		};
	} catch (error) {
		// Make sure we close the handle even if there's an error
		try {
			if (tokenHandle) {
				closeHandleFn(tokenHandle);
			}
		} catch (closeError) {
			console.error('Error closing handle:', closeError);
		}

		throw error;
	}
}

React Router でユーザ名を使用する

今回は React Router なので、サーバサイドで処理するため loader にユーザ名を取得する処理を追加します。認証できなかった時はユーザ名に"unknown"を返すようにしていますので、適宜変更してください。
getUserInfoFromToken は クライアントサイドでは使用できないため、loader 内でインポートする必要があります。

root.tsx

// ローダー
export async function loader({ request }: LoaderFunctionArgs) {
	try {
		// x-iis-windowsauthtoken ヘッダーを取得
		const tokenHeader = request.headers.get("x-iis-windowsauthtoken");

		if (!tokenHeader) {
			console.error("No x-iis-windowsauthtoken header found");
			return { userId: "unknown", error: "No auth token found" };
		}

		// トークンを16進数から数値に変換
		const handle = parseInt(tokenHeader, 16);

		if (isNaN(handle)) {
			console.error("Invalid token format in header");
			return { userId: "unknown", error: "Invalid token format" };
		}

		// Windows認証情報を取得
        const { getUserInfoFromToken } = await import("./utilities/windows-auth");
        const userInfo: UserInfo = getUserInfoFromToken(handle);
		const userId = userInfo.domain ? `${userInfo.domain}\\${userInfo.user}` : userInfo.user;

		console.log(`Authenticated user: ${userId}`);

		return { userId, userInfo };
	} catch (error) {
		console.error("Error in loader:", error);
		return {
			userId: "unknown",
			error: error instanceof Error ? error.message : "Unknown error",
		};
	}
}

...

// メインアプリケーションコンポーネント
export default function App() {
	const loaderData = useLoaderData<typeof loader>();

	const isAuthenticated = loaderData.userId && loaderData.userId !== "unknown";

	return (
		<div className="flex flex-col h-screen">
			{isAuthenticated && (
                <div>{loaderData.userId}</div>
                <Outlet />
			)}
			{!isAuthenticated && loaderData.userId === "unknown" && (
                <div>認証エラー</div>
                {loaderData.error && <div>Error: {loaderData.error}</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?