概要
でホストしたIISの環境で、統合windows認証してみました。
React Rouver v7でやってますが、Next.js でも同じようにできるはず。
参考にしたサイト
IIS の設定
サイトの「認証」で「Windows 認証」を有効、「匿名認証」を無効にします。

web.config の httpPlatform のプロパティに
forwardWindowsAuthToken="true"
を追加します。これにより、HTTPリクエストヘッダーに x-iis-windowsauthtoken が追加されます。
<?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はログファイルに記録されるので、適宜削除してください。
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 内でインポートする必要があります。
// ローダー
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>
);
}