swallowkit 1.0.0-beta.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +87 -181
- package/README.md +87 -238
- package/dist/__tests__/fixtures.d.ts +12 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +101 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/cli/commands/add-auth.d.ts +15 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +790 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +164 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +20 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +27 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
- package/dist/cli/commands/dev-seeds.js +188 -6
- package/dist/cli/commands/dev-seeds.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +30 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +495 -129
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +69 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +788 -766
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +24 -3
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +408 -189
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +335 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +13 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +151 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/auth-operations.d.ts +61 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +272 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +185 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/file-session.d.ts +56 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +203 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +37 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +248 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +51 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +187 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/agent-instructions.d.ts +29 -0
- package/dist/core/project/agent-instructions.d.ts.map +1 -0
- package/dist/core/project/agent-instructions.js +160 -0
- package/dist/core/project/agent-instructions.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/capabilities.d.ts +58 -0
- package/dist/core/project/capabilities.d.ts.map +1 -0
- package/dist/core/project/capabilities.js +135 -0
- package/dist/core/project/capabilities.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +188 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +274 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +299 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +63 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1774 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1029 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +4 -3
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +405 -134
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +229 -78
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +753 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +152 -8
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +17 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +174 -34
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +69 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +412 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/machine/contracts.d.ts +24 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +14 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +42 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +566 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +425 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +105 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +7 -11
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +42 -17
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +21 -0
- package/dist/utils/python-uv.d.ts.map +1 -0
- package/dist/utils/python-uv.js +111 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +49 -0
- package/dist/version.js.map +1 -0
- package/package.json +15 -17
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
- package/src/__tests__/agent-instructions.test.ts +237 -0
- package/src/__tests__/agent-loop-phase34.test.ts +513 -0
- package/src/__tests__/auth.test.ts +770 -0
- package/src/__tests__/bff-swa-auth.test.ts +275 -0
- package/src/__tests__/config.test.ts +176 -3
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +459 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
- package/src/__tests__/dev-seeds.test.ts +124 -0
- package/src/__tests__/dev.test.ts +254 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/fixtures.ts +101 -0
- package/src/__tests__/functions-generator.test.ts +137 -1
- package/src/__tests__/init.test.ts +366 -2
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +371 -0
- package/src/__tests__/mcp.test.ts +253 -0
- package/src/__tests__/model-parser.test.ts +142 -0
- package/src/__tests__/multi-auth.test.ts +160 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/__tests__/package-manager.test.ts +59 -4
- package/src/__tests__/plan-apply.test.ts +499 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +287 -24
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +104 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +853 -0
- package/src/cli/commands/add-connector.ts +159 -0
- package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
- package/src/cli/commands/create-model.ts +21 -100
- package/src/cli/commands/dev-seeds.ts +278 -7
- package/src/cli/commands/dev.ts +607 -160
- package/src/cli/commands/init.ts +851 -786
- package/src/cli/commands/scaffold.ts +531 -236
- package/src/cli/index.ts +338 -60
- package/src/core/config.ts +145 -9
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/auth-operations.ts +360 -0
- package/src/core/operations/create-model.ts +191 -0
- package/src/core/operations/file-session.ts +222 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +258 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +77 -0
- package/src/core/operations/scaffold-plan.ts +215 -0
- package/src/core/project/agent-instructions.ts +167 -0
- package/src/core/project/agent-templates/AGENTS.md +504 -0
- package/src/core/project/agent-templates/CLAUDE.md +31 -0
- package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
- package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
- package/src/core/project/agent-templates/copilot-instructions.md +27 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/capabilities.ts +193 -0
- package/src/core/project/drift.ts +202 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/validation.ts +295 -0
- package/src/core/project/workflows.ts +295 -0
- package/src/core/scaffold/auth-generator.ts +1824 -0
- package/src/core/scaffold/connector-functions-generator.ts +1133 -0
- package/src/core/scaffold/functions-generator.ts +432 -134
- package/src/core/scaffold/model-parser.ts +244 -90
- package/src/core/scaffold/native-schema-generator.ts +904 -0
- package/src/core/scaffold/nextjs-generator.ts +174 -9
- package/src/core/scaffold/ui-generator.ts +191 -36
- package/src/core/verify/index.ts +468 -0
- package/src/machine/contracts.ts +33 -0
- package/src/machine/errors.ts +45 -0
- package/src/machine/index.ts +690 -0
- package/src/mcp/index.ts +423 -0
- package/src/types/index.ts +137 -0
- package/src/utils/package-manager.ts +41 -19
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
|
@@ -0,0 +1,1774 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SwallowKit 認証・認可コード生成
|
|
4
|
+
* add-auth コマンドおよび scaffold ロールガード挿入で使用するテンプレート群
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.generateNamedAuthRouterTS = generateNamedAuthRouterTS;
|
|
8
|
+
exports.generateNamedExternalTokenVerifierCSharp = generateNamedExternalTokenVerifierCSharp;
|
|
9
|
+
exports.generateNamedExternalTokenVerifierPython = generateNamedExternalTokenVerifierPython;
|
|
10
|
+
exports.generateNamedAuthRouterCSharp = generateNamedAuthRouterCSharp;
|
|
11
|
+
exports.generateNamedAuthRouterPython = generateNamedAuthRouterPython;
|
|
12
|
+
exports.generateAuthModels = generateAuthModels;
|
|
13
|
+
exports.generateAuthFunctionsTS = generateAuthFunctionsTS;
|
|
14
|
+
exports.generateJwtHelperTS = generateJwtHelperTS;
|
|
15
|
+
exports.generateAuthFunctionsCSharp = generateAuthFunctionsCSharp;
|
|
16
|
+
exports.generateJwtHelperCSharp = generateJwtHelperCSharp;
|
|
17
|
+
exports.generateAuthFunctionsPython = generateAuthFunctionsPython;
|
|
18
|
+
exports.generateJwtHelperPython = generateJwtHelperPython;
|
|
19
|
+
exports.generateBFFAuthLoginRoute = generateBFFAuthLoginRoute;
|
|
20
|
+
exports.generateBFFAuthLogoutRoute = generateBFFAuthLogoutRoute;
|
|
21
|
+
exports.generateBFFAuthMeRoute = generateBFFAuthMeRoute;
|
|
22
|
+
exports.generateProxy = generateProxy;
|
|
23
|
+
exports.generateLoginPage = generateLoginPage;
|
|
24
|
+
exports.generateAuthContext = generateAuthContext;
|
|
25
|
+
exports.generateBFFCallFunctionWithAuth = generateBFFCallFunctionWithAuth;
|
|
26
|
+
exports.generateBFFCallFunctionWithSwaAuth = generateBFFCallFunctionWithSwaAuth;
|
|
27
|
+
exports.generateBFFCallFunctionWithMultipleAuth = generateBFFCallFunctionWithMultipleAuth;
|
|
28
|
+
exports.generateSwaAuthHelperTS = generateSwaAuthHelperTS;
|
|
29
|
+
exports.generateExternalTokenAdapter = generateExternalTokenAdapter;
|
|
30
|
+
exports.generateAuthenticatedFetch = generateAuthenticatedFetch;
|
|
31
|
+
exports.generateExternalTokenBFFMeRoute = generateExternalTokenBFFMeRoute;
|
|
32
|
+
exports.generateExternalTokenAuthContext = generateExternalTokenAuthContext;
|
|
33
|
+
exports.generateExternalTokenVerifierTS = generateExternalTokenVerifierTS;
|
|
34
|
+
exports.generateExternalTokenHelperTS = generateExternalTokenHelperTS;
|
|
35
|
+
exports.generateExternalAuthMeTS = generateExternalAuthMeTS;
|
|
36
|
+
exports.generateExternalTokenVerifierCSharp = generateExternalTokenVerifierCSharp;
|
|
37
|
+
exports.generateExternalTokenHelperCSharp = generateExternalTokenHelperCSharp;
|
|
38
|
+
exports.generateExternalAuthMeCSharp = generateExternalAuthMeCSharp;
|
|
39
|
+
exports.generateExternalTokenVerifierPython = generateExternalTokenVerifierPython;
|
|
40
|
+
exports.generateExternalTokenHelperPython = generateExternalTokenHelperPython;
|
|
41
|
+
exports.generateExternalAuthMePython = generateExternalAuthMePython;
|
|
42
|
+
exports.generateSwaAuthHelperPython = generateSwaAuthHelperPython;
|
|
43
|
+
exports.generateSwaAuthHelperCSharp = generateSwaAuthHelperCSharp;
|
|
44
|
+
exports.generateAuthImportTS = generateAuthImportTS;
|
|
45
|
+
exports.generateAuthGuardTS = generateAuthGuardTS;
|
|
46
|
+
exports.generateAuthGuardCSharp = generateAuthGuardCSharp;
|
|
47
|
+
exports.generateAuthGuardPython = generateAuthGuardPython;
|
|
48
|
+
function generateNamedAuthRouterTS(config) {
|
|
49
|
+
const external = Object.entries(config.schemes).filter(([, s]) => s.provider === "external-token");
|
|
50
|
+
const custom = Object.entries(config.schemes).filter(([, s]) => s.provider === "custom-jwt");
|
|
51
|
+
const imports = external.map(([name]) => {
|
|
52
|
+
const slug = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
53
|
+
const id = safeIdentifier(name);
|
|
54
|
+
return `import { verifyExternalToken as verify_${id}, ExternalTokenVerificationError as Error_${id} } from './schemes/${slug}/verifier';`;
|
|
55
|
+
}).concat(custom.map(([name]) => {
|
|
56
|
+
const slug = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
57
|
+
return `import { requireAuth as verify_${safeIdentifier(name)} } from './schemes/${slug}/jwt-helper';`;
|
|
58
|
+
})).join("\n");
|
|
59
|
+
const verifiers = external.map(([name]) => { const id = safeIdentifier(name); return `${JSON.stringify(name)}: async (token: string) => { try { return await verify_${id}(token); } catch (e) { if (e instanceof Error_${id}) throw new AuthError(e.status, e.message); throw e; } }`; }).join(",\n ");
|
|
60
|
+
return `import type { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
61
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- generated multi-scheme router works with dynamically shaped principals */
|
|
62
|
+
${imports}
|
|
63
|
+
export type AuthPrincipal = { subject: string; scheme: string; issuer: string; roles: string[]; claims: Record<string, unknown>; /** @deprecated */ userId?: string; /** @deprecated */ userDetails?: string };
|
|
64
|
+
export class AuthError extends Error { constructor(public status: 401 | 403 | 503, message: string) { super(message); } }
|
|
65
|
+
const schemes = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n, s]) => [n, { provider: s.provider, allowedProviders: s.swa?.allowedProviders ?? [] }])))} as const;
|
|
66
|
+
const policies = ${JSON.stringify(config.authorization.policies)} as Record<string, { schemes: string[]; roles?: string[] }>;
|
|
67
|
+
const externalVerifiers: Record<string, (token: string) => Promise<any>> = { ${verifiers} };
|
|
68
|
+
const customVerifiers: Record<string, (request: HttpRequest) => any> = { ${custom.map(([name]) => `${JSON.stringify(name)}: verify_${safeIdentifier(name)}`).join(", ")} };
|
|
69
|
+
function swa(request: HttpRequest, scheme: string): AuthPrincipal {
|
|
70
|
+
const encoded = request.headers.get('x-ms-client-principal');
|
|
71
|
+
if (!encoded) throw new AuthError(401, 'Authentication required');
|
|
72
|
+
try { const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) as Record<string, unknown>;
|
|
73
|
+
const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r): r is string => typeof r === 'string') : [];
|
|
74
|
+
if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
|
|
75
|
+
const allowed = (schemes as any)[scheme]?.allowedProviders as string[] | undefined;
|
|
76
|
+
if (allowed?.length && (typeof value.identityProvider !== 'string' || !allowed.includes(value.identityProvider))) throw new Error();
|
|
77
|
+
return { subject: value.userId, scheme, issuer: typeof value.identityProvider === 'string' ? 'swa:' + value.identityProvider : 'swa', roles, claims: value, userId: value.userId, userDetails: typeof value.userDetails === 'string' ? value.userDetails : undefined };
|
|
78
|
+
} catch (e) { if (e instanceof AuthError) throw e; throw new AuthError(401, 'Invalid SWA client principal'); }
|
|
79
|
+
}
|
|
80
|
+
export async function requireAuth(request: HttpRequest, policyName: string): Promise<AuthPrincipal> {
|
|
81
|
+
const policy = policies[policyName]; if (!policy) throw new AuthError(401, 'Undefined authorization policy');
|
|
82
|
+
const bearer = request.headers.get('authorization'); const hasSwa = Boolean(request.headers.get('x-ms-client-principal'));
|
|
83
|
+
const candidates = policy.schemes.filter(name => { const p=(schemes as any)[name]?.provider; return p === 'swa' ? hasSwa : (p === 'external-token' || p === 'custom-jwt') ? Boolean(bearer?.startsWith('Bearer ')) : false; });
|
|
84
|
+
if (candidates.length !== 1) throw new AuthError(401, candidates.length ? 'Ambiguous credentials' : 'Authentication required');
|
|
85
|
+
const scheme = candidates[0]; const provider = (schemes as any)[scheme]?.provider; let principal: AuthPrincipal;
|
|
86
|
+
if (provider === 'swa') principal = swa(request, scheme);
|
|
87
|
+
else if (provider === 'external-token') { const token=bearer!.slice(7); try { const p=await externalVerifiers[scheme](token); const subject=p?.subject ?? p?.userId; if (typeof subject !== 'string' || !subject || !Array.isArray(p.roles)) throw new AuthError(401,'Invalid external principal'); principal={subject,scheme,issuer:typeof p.issuer==='string'?p.issuer:scheme,roles:p.roles,claims:p.claims && typeof p.claims==='object'?p.claims:p,userId:subject,userDetails:p.userDetails}; } catch(e) { if(e instanceof AuthError) throw e; throw new AuthError(503,'Identity provider unavailable'); } }
|
|
88
|
+
else if (provider === 'custom-jwt') { try { const p=customVerifiers[scheme](request); const subject=p?.sub ?? p?.userId; if(typeof subject !== 'string' || !subject || !Array.isArray(p.roles)) throw new Error(); principal={subject,scheme,issuer:scheme,roles:p.roles,claims:p,userId:subject,userDetails:p.loginId}; } catch { throw new AuthError(401,'Invalid or expired token'); } }
|
|
89
|
+
else throw new AuthError(401, 'Authentication adapter is not configured');
|
|
90
|
+
requireRoles(principal, policy.roles || []); return principal;
|
|
91
|
+
}
|
|
92
|
+
export function requireRoles(principal: AuthPrincipal, roles: string[]): void { if (roles.length && !roles.some(r => principal.roles.includes(r))) throw new AuthError(403, 'Insufficient permissions'); }
|
|
93
|
+
export function identityKey(principal: AuthPrincipal): string { return principal.scheme + ':' + principal.subject; }
|
|
94
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status:error.status, jsonBody:{error:error.message} } : null; }
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
function safeIdentifier(name) {
|
|
98
|
+
return name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
99
|
+
}
|
|
100
|
+
/** C# の string[] リテラル。空のときは型推論不能な `new[] { }` (CS0826) を避けて Array.Empty<string>() を出力する。 */
|
|
101
|
+
function csharpStringArray(items) {
|
|
102
|
+
return items.length > 0 ? `new[] { ${items.map((item) => `"${item}"`).join(", ")} }` : "Array.Empty<string>()";
|
|
103
|
+
}
|
|
104
|
+
function schemeSlug(name) {
|
|
105
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
106
|
+
}
|
|
107
|
+
function generateNamedExternalTokenVerifierCSharp(scheme) {
|
|
108
|
+
const id = safeIdentifier(scheme);
|
|
109
|
+
return `namespace Functions.Auth.Schemes;
|
|
110
|
+
public record ExternalPrincipal_${id}(string Subject, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims);
|
|
111
|
+
public sealed class ExternalTokenVerificationException_${id} : Exception
|
|
112
|
+
{
|
|
113
|
+
public bool IsIdentityProviderUnavailable { get; }
|
|
114
|
+
public ExternalTokenVerificationException_${id}(string message, bool unavailable = false) : base(message) => IsIdentityProviderUnavailable = unavailable;
|
|
115
|
+
}
|
|
116
|
+
public static class ExternalTokenVerifier_${id}
|
|
117
|
+
{
|
|
118
|
+
// Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
|
|
119
|
+
public static Task<ExternalPrincipal_${id}> VerifyAsync(string token) => throw new InvalidOperationException("External token verifier for ${scheme} is not configured");
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
function generateNamedExternalTokenVerifierPython(scheme) {
|
|
124
|
+
return `class ExternalTokenVerificationError(Exception):
|
|
125
|
+
def __init__(self, message: str, identity_provider_unavailable: bool = False):
|
|
126
|
+
super().__init__(message)
|
|
127
|
+
self.identity_provider_unavailable = identity_provider_unavailable
|
|
128
|
+
|
|
129
|
+
def verify_external_token(token: str) -> dict:
|
|
130
|
+
# Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
|
|
131
|
+
raise RuntimeError("External token verifier for ${scheme} is not configured")
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
function generateNamedAuthRouterCSharp(config) {
|
|
135
|
+
const externalCases = Object.entries(config.schemes).filter(([, s]) => s.provider === "external-token").map(([name]) => {
|
|
136
|
+
const id = safeIdentifier(name);
|
|
137
|
+
return ` case "${name}":
|
|
138
|
+
try
|
|
139
|
+
{
|
|
140
|
+
var external = await ExternalTokenVerifier_${id}.VerifyAsync(bearer!);
|
|
141
|
+
principal = new AuthPrincipal(external.Subject, "${name}", external.Issuer, external.Roles, external.Claims);
|
|
142
|
+
}
|
|
143
|
+
catch (ExternalTokenVerificationException_${id} ex) when (ex.IsIdentityProviderUnavailable) { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "Identity provider unavailable")); }
|
|
144
|
+
catch (ExternalTokenVerificationException_${id}) { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid token")); }
|
|
145
|
+
catch { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "External token verifier unavailable")); }
|
|
146
|
+
break;`;
|
|
147
|
+
}).join("\n");
|
|
148
|
+
return `using System.Net;
|
|
149
|
+
using System.Text;
|
|
150
|
+
using System.Text.Json;
|
|
151
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
152
|
+
using Functions.Auth.Schemes;
|
|
153
|
+
namespace Functions.Auth;
|
|
154
|
+
public record AuthPrincipal(string Subject, string Scheme, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims)
|
|
155
|
+
{
|
|
156
|
+
[Obsolete("Use Subject")] public string UserId => Subject;
|
|
157
|
+
public string IdentityKey => $"{Scheme}:{Subject}";
|
|
158
|
+
}
|
|
159
|
+
public static class AuthRouter
|
|
160
|
+
{
|
|
161
|
+
private static readonly Dictionary<string, (string[] Schemes, string[] Roles)> Policies = new()
|
|
162
|
+
{
|
|
163
|
+
${Object.entries(config.authorization.policies).map(([n, p]) => ` ["${n}"] = (${csharpStringArray(p.schemes)}, ${csharpStringArray(p.roles ?? [])}),`).join("\n")}
|
|
164
|
+
};
|
|
165
|
+
private static readonly Dictionary<string,string> Providers = new() { ${Object.entries(config.schemes).map(([n, s]) => `["${n}"]="${s.provider}"`).join(", ")} };
|
|
166
|
+
private static readonly Dictionary<string,string[]> AllowedProviders = new() { ${Object.entries(config.schemes).map(([n, s]) => `["${n}"]=${csharpStringArray(s.swa?.allowedProviders ?? [])}`).join(", ")} };
|
|
167
|
+
public static async Task<(AuthPrincipal? Principal, HttpResponseData? ErrorResponse)> Authorize(HttpRequestData request, string policyName)
|
|
168
|
+
{
|
|
169
|
+
if (!Policies.TryGetValue(policyName, out var policy)) return (null, await Error(request, HttpStatusCode.Unauthorized, "Undefined authorization policy"));
|
|
170
|
+
var bearerHeader = request.Headers.TryGetValues("Authorization", out var authValues) ? authValues.FirstOrDefault() : null;
|
|
171
|
+
var bearer = bearerHeader?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) == true ? bearerHeader[7..] : null;
|
|
172
|
+
var swa = request.Headers.TryGetValues("x-ms-client-principal", out var swaValues) ? swaValues.FirstOrDefault() : null;
|
|
173
|
+
var candidates = policy.Schemes.Where(s => Providers[s] == "swa" ? swa != null : Providers[s] is "external-token" or "custom-jwt" ? bearer != null : false).ToArray();
|
|
174
|
+
if (candidates.Length != 1) return (null, await Error(request, HttpStatusCode.Unauthorized, candidates.Length == 0 ? "Authentication required" : "Ambiguous credentials"));
|
|
175
|
+
var scheme = candidates[0]; AuthPrincipal principal;
|
|
176
|
+
switch (scheme)
|
|
177
|
+
{
|
|
178
|
+
${externalCases}
|
|
179
|
+
default:
|
|
180
|
+
if (Providers[scheme] != "swa") return (null, await Error(request, HttpStatusCode.Unauthorized, "Authentication adapter is not configured"));
|
|
181
|
+
try
|
|
182
|
+
{
|
|
183
|
+
using var doc = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(swa!)));
|
|
184
|
+
var root = doc.RootElement; var subject = root.GetProperty("userId").GetString();
|
|
185
|
+
var roles = root.TryGetProperty("userRoles", out var rv) ? rv.EnumerateArray().Where(v=>v.ValueKind==JsonValueKind.String).Select(v=>v.GetString()!).ToArray() : Array.Empty<string>();
|
|
186
|
+
if (string.IsNullOrEmpty(subject) || !roles.Contains("authenticated")) throw new InvalidOperationException();
|
|
187
|
+
var identityProvider = root.TryGetProperty("identityProvider", out var iv) ? iv.GetString() : null;
|
|
188
|
+
if (AllowedProviders[scheme].Length > 0 && (identityProvider == null || !AllowedProviders[scheme].Contains(identityProvider))) throw new InvalidOperationException();
|
|
189
|
+
var issuer = identityProvider == null ? "swa" : "swa:" + identityProvider;
|
|
190
|
+
principal = new AuthPrincipal(subject!, scheme, issuer, roles, new Dictionary<string,object?> { ["clientPrincipal"] = root.Clone() });
|
|
191
|
+
}
|
|
192
|
+
catch { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid SWA client principal")); }
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
if (policy.Roles.Length > 0 && !policy.Roles.Any(principal.Roles.Contains)) return (null, await Error(request, HttpStatusCode.Forbidden, "Insufficient permissions"));
|
|
196
|
+
return (principal, null);
|
|
197
|
+
}
|
|
198
|
+
private static async Task<HttpResponseData> Error(HttpRequestData request, HttpStatusCode status, string message) { var response=request.CreateResponse(status); await response.WriteAsJsonAsync(new { error=message }); return response; }
|
|
199
|
+
}
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
function generateNamedAuthRouterPython(config) {
|
|
203
|
+
const external = Object.entries(config.schemes).filter(([, s]) => s.provider === "external-token");
|
|
204
|
+
const imports = external.map(([n]) => `from auth.schemes.${schemeSlug(n).replace(/-/g, "_")}.verifier import verify_external_token as verify_${safeIdentifier(n)}, ExternalTokenVerificationError as Error_${safeIdentifier(n)}`).join("\n");
|
|
205
|
+
const cases = external.map(([n], index) => ` ${index === 0 ? "if" : "elif"} scheme == ${JSON.stringify(n)}:
|
|
206
|
+
try:
|
|
207
|
+
value = verify_${safeIdentifier(n)}(bearer)
|
|
208
|
+
except Error_${safeIdentifier(n)} as exc:
|
|
209
|
+
raise AuthError(503 if exc.identity_provider_unavailable else 401, "Identity provider unavailable" if exc.identity_provider_unavailable else "Invalid token")
|
|
210
|
+
except Exception:
|
|
211
|
+
raise AuthError(503, "External token verifier unavailable")
|
|
212
|
+
if not value.get("subject") or not isinstance(value.get("roles"), list): raise AuthError(401, "Invalid external principal")
|
|
213
|
+
principal = {"subject": value["subject"], "scheme": scheme, "issuer": value.get("issuer", scheme), "roles": value["roles"], "claims": value.get("claims", {}), "userId": value["subject"]}`).join("\n");
|
|
214
|
+
return `import base64
|
|
215
|
+
import json
|
|
216
|
+
import azure.functions as func
|
|
217
|
+
${imports}
|
|
218
|
+
POLICIES = ${JSON.stringify(config.authorization.policies)}
|
|
219
|
+
PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n, s]) => [n, s.provider])))}
|
|
220
|
+
ALLOWED_PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n, s]) => [n, s.swa?.allowedProviders ?? []])))}
|
|
221
|
+
class AuthError(Exception):
|
|
222
|
+
def __init__(self, status, message): self.status, self.message = status, message
|
|
223
|
+
def require_auth(req: func.HttpRequest, policy_name: str):
|
|
224
|
+
policy = POLICIES.get(policy_name)
|
|
225
|
+
if not policy: raise AuthError(401, "Undefined authorization policy")
|
|
226
|
+
header = req.headers.get("authorization", "")
|
|
227
|
+
bearer = header[7:] if header.lower().startswith("bearer ") else None
|
|
228
|
+
swa = req.headers.get("x-ms-client-principal")
|
|
229
|
+
candidates = [s for s in policy["schemes"] if (PROVIDERS[s] == "swa" and swa) or (PROVIDERS[s] in ("external-token", "custom-jwt") and bearer)]
|
|
230
|
+
if len(candidates) != 1: raise AuthError(401, "Authentication required" if not candidates else "Ambiguous credentials")
|
|
231
|
+
scheme = candidates[0]
|
|
232
|
+
${cases}
|
|
233
|
+
${external.length ? "elif" : "if"} PROVIDERS[scheme] == "swa":
|
|
234
|
+
try:
|
|
235
|
+
value=json.loads(base64.b64decode(swa).decode("utf-8")); roles=[r for r in value.get("userRoles",[]) if isinstance(r,str)]
|
|
236
|
+
if not value.get("userId") or "authenticated" not in roles: raise ValueError()
|
|
237
|
+
if ALLOWED_PROVIDERS[scheme] and value.get("identityProvider") not in ALLOWED_PROVIDERS[scheme]: raise ValueError()
|
|
238
|
+
principal={"subject":value["userId"],"scheme":scheme,"issuer":"swa:"+value.get("identityProvider", "unknown"),"roles":roles,"claims":value,"userId":value["userId"]}
|
|
239
|
+
except Exception: raise AuthError(401,"Invalid SWA client principal")
|
|
240
|
+
else: raise AuthError(401,"Authentication adapter is not configured")
|
|
241
|
+
require_roles(principal, policy.get("roles", [])); return principal
|
|
242
|
+
def require_roles(principal, roles):
|
|
243
|
+
if roles and not any(role in principal.get("roles",[]) for role in roles): raise AuthError(403,"Insufficient permissions")
|
|
244
|
+
def identity_key(principal): return principal["scheme"] + ":" + principal["subject"]
|
|
245
|
+
def handle_auth_error(error): return func.HttpResponse(json.dumps({"error":error.message}),status_code=error.status,mimetype="application/json") if isinstance(error,AuthError) else None
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
// ============================================================
|
|
249
|
+
// 1. shared/models/auth.ts(Zod スキーマ)
|
|
250
|
+
// ============================================================
|
|
251
|
+
function generateAuthModels() {
|
|
252
|
+
return `import { z } from 'zod/v4';
|
|
253
|
+
|
|
254
|
+
// ログインリクエスト
|
|
255
|
+
export const LoginRequest = z.object({
|
|
256
|
+
loginId: z.string().min(1),
|
|
257
|
+
password: z.string().min(1),
|
|
258
|
+
});
|
|
259
|
+
export type LoginRequest = z.infer<typeof LoginRequest>;
|
|
260
|
+
|
|
261
|
+
// 認証済みユーザー情報(JWT claims 相当、機密情報なし)
|
|
262
|
+
export const AuthUser = z.object({
|
|
263
|
+
id: z.string(),
|
|
264
|
+
loginId: z.string(),
|
|
265
|
+
name: z.string(),
|
|
266
|
+
email: z.string().email(),
|
|
267
|
+
roles: z.array(z.string()),
|
|
268
|
+
});
|
|
269
|
+
export type AuthUser = z.infer<typeof AuthUser>;
|
|
270
|
+
|
|
271
|
+
// ログインレスポンス
|
|
272
|
+
export const LoginResponse = z.object({
|
|
273
|
+
user: AuthUser,
|
|
274
|
+
token: z.string(),
|
|
275
|
+
expiresAt: z.string(),
|
|
276
|
+
});
|
|
277
|
+
export type LoginResponse = z.infer<typeof LoginResponse>;
|
|
278
|
+
|
|
279
|
+
export const displayName = 'Auth';
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
// ============================================================
|
|
283
|
+
// 2. TypeScript Functions テンプレート
|
|
284
|
+
// ============================================================
|
|
285
|
+
function generateAuthFunctionsTS(sharedPackageName, config, provider) {
|
|
286
|
+
const envVar = `${config.userConnector.toUpperCase()}_CONNECTION_STRING`;
|
|
287
|
+
const driverImport = provider === "mysql"
|
|
288
|
+
? `import mysql from 'mysql2/promise';`
|
|
289
|
+
: provider === "postgres"
|
|
290
|
+
? `import pg from 'pg';`
|
|
291
|
+
: `import sql from 'mssql';`;
|
|
292
|
+
const getConnection = provider === "mysql"
|
|
293
|
+
? `function getConnection() {
|
|
294
|
+
return mysql.createConnection(process.env.${envVar} || '');
|
|
295
|
+
}`
|
|
296
|
+
: provider === "postgres"
|
|
297
|
+
? `async function getConnection() {
|
|
298
|
+
const client = new pg.Client(process.env.${envVar} || '');
|
|
299
|
+
await client.connect();
|
|
300
|
+
return client;
|
|
301
|
+
}`
|
|
302
|
+
: `async function getConnection() {
|
|
303
|
+
return sql.connect(process.env.${envVar} || '');
|
|
304
|
+
}`;
|
|
305
|
+
const connVar = provider === "mysql" ? "conn" : provider === "postgres" ? "client" : "pool";
|
|
306
|
+
const queryExec = provider === "mysql"
|
|
307
|
+
? `const [rows] = await ${connVar}.query(
|
|
308
|
+
'SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = ?',
|
|
309
|
+
[body.loginId]
|
|
310
|
+
);
|
|
311
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mysql2 rows are dynamically shaped
|
|
312
|
+
const users = rows as any[];`
|
|
313
|
+
: provider === "postgres"
|
|
314
|
+
? `const result = await ${connVar}.query(
|
|
315
|
+
'SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = $1',
|
|
316
|
+
[body.loginId]
|
|
317
|
+
);
|
|
318
|
+
const users = result.rows;`
|
|
319
|
+
: `const result = await ${connVar}.request()
|
|
320
|
+
.input('loginId', body.loginId)
|
|
321
|
+
.query('SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = @loginId');
|
|
322
|
+
const users = result.recordset;`;
|
|
323
|
+
const cleanup = provider === "mysql"
|
|
324
|
+
? `await ${connVar}.end();`
|
|
325
|
+
: provider === "postgres"
|
|
326
|
+
? `await ${connVar}.end();`
|
|
327
|
+
: `await ${connVar}.close();`;
|
|
328
|
+
return `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
329
|
+
import { LoginRequest, LoginResponse, AuthUser } from '${sharedPackageName}';
|
|
330
|
+
import { requireAuth, generateToken, handleAuthError } from './auth/jwt-helper';
|
|
331
|
+
import bcrypt from 'bcryptjs';
|
|
332
|
+
${driverImport}
|
|
333
|
+
|
|
334
|
+
${getConnection}
|
|
335
|
+
|
|
336
|
+
// POST /api/auth/login - ログイン
|
|
337
|
+
app.http('auth-login', {
|
|
338
|
+
methods: ['POST'],
|
|
339
|
+
route: 'auth/login',
|
|
340
|
+
authLevel: 'function',
|
|
341
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
342
|
+
try {
|
|
343
|
+
const body = LoginRequest.parse(await request.json());
|
|
344
|
+
|
|
345
|
+
const ${connVar} = await getConnection();
|
|
346
|
+
try {
|
|
347
|
+
${queryExec}
|
|
348
|
+
|
|
349
|
+
if (users.length === 0) {
|
|
350
|
+
return { status: 401, jsonBody: { error: 'Invalid credentials' } };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const user = users[0];
|
|
354
|
+
const valid = await bcrypt.compare(body.password, user.${config.passwordHashColumn});
|
|
355
|
+
if (!valid) {
|
|
356
|
+
return { status: 401, jsonBody: { error: 'Invalid credentials' } };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ロール取得(JSON配列 or カンマ区切り)
|
|
360
|
+
let roles: string[] = [];
|
|
361
|
+
if (typeof user.${config.rolesColumn} === 'string') {
|
|
362
|
+
try {
|
|
363
|
+
roles = JSON.parse(user.${config.rolesColumn});
|
|
364
|
+
} catch {
|
|
365
|
+
roles = user.${config.rolesColumn}.split(',').map((r: string) => r.trim());
|
|
366
|
+
}
|
|
367
|
+
} else if (Array.isArray(user.${config.rolesColumn})) {
|
|
368
|
+
roles = user.${config.rolesColumn};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const authUser: AuthUser = {
|
|
372
|
+
id: String(user.id),
|
|
373
|
+
loginId: user.${config.loginIdColumn},
|
|
374
|
+
name: user.name || user.${config.loginIdColumn},
|
|
375
|
+
email: user.email || '',
|
|
376
|
+
roles,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const token = generateToken(authUser);
|
|
380
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
381
|
+
|
|
382
|
+
const response: LoginResponse = { user: authUser, token, expiresAt };
|
|
383
|
+
return { status: 200, jsonBody: response };
|
|
384
|
+
} finally {
|
|
385
|
+
${cleanup}
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
context.error('Login error:', error);
|
|
389
|
+
return { status: 500, jsonBody: { error: 'Login failed' } };
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// GET /api/auth/me - 現在のユーザー情報取得
|
|
395
|
+
app.http('auth-me', {
|
|
396
|
+
methods: ['GET'],
|
|
397
|
+
route: 'auth/me',
|
|
398
|
+
authLevel: 'function',
|
|
399
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
400
|
+
try {
|
|
401
|
+
const user = requireAuth(request);
|
|
402
|
+
return { status: 200, jsonBody: user };
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const authErr = handleAuthError(error);
|
|
405
|
+
if (authErr) return authErr;
|
|
406
|
+
context.error('Auth/me error:', error);
|
|
407
|
+
return { status: 500, jsonBody: { error: 'Internal server error' } };
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// POST /api/auth/logout - ログアウト(ステートレスJWTのためサーバー側処理なし)
|
|
413
|
+
app.http('auth-logout', {
|
|
414
|
+
methods: ['POST'],
|
|
415
|
+
route: 'auth/logout',
|
|
416
|
+
authLevel: 'function',
|
|
417
|
+
handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
|
|
418
|
+
return { status: 200, jsonBody: { message: 'Logged out' } };
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
// ============================================================
|
|
424
|
+
// 3. TypeScript JWT Helper
|
|
425
|
+
// ============================================================
|
|
426
|
+
function generateJwtHelperTS() {
|
|
427
|
+
return `import jwt from 'jsonwebtoken';
|
|
428
|
+
import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
429
|
+
|
|
430
|
+
export interface JwtPayload {
|
|
431
|
+
sub: string;
|
|
432
|
+
loginId: string;
|
|
433
|
+
name: string;
|
|
434
|
+
email: string;
|
|
435
|
+
roles: string[];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const JWT_SECRET = process.env.JWT_SECRET || '';
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* JWT 検証。無効な場合は AuthError をスロー。
|
|
442
|
+
*/
|
|
443
|
+
export function requireAuth(request: HttpRequest): JwtPayload {
|
|
444
|
+
const authHeader = request.headers.get('authorization');
|
|
445
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
446
|
+
throw new AuthError(401, 'Missing or invalid Authorization header');
|
|
447
|
+
}
|
|
448
|
+
const token = authHeader.slice(7);
|
|
449
|
+
try {
|
|
450
|
+
const payload = jwt.verify(token, JWT_SECRET) as JwtPayload;
|
|
451
|
+
return payload;
|
|
452
|
+
} catch {
|
|
453
|
+
throw new AuthError(401, 'Invalid or expired token');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* ロール確認。必要なロールを持たない場合は AuthError をスロー。
|
|
459
|
+
*/
|
|
460
|
+
export function requireRoles(user: JwtPayload, requiredRoles: string[]): void {
|
|
461
|
+
const hasRole = requiredRoles.some(role => user.roles.includes(role));
|
|
462
|
+
if (!hasRole) {
|
|
463
|
+
throw new AuthError(403, \`Requires one of roles: \${requiredRoles.join(', ')}\`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* JWT 生成
|
|
469
|
+
*/
|
|
470
|
+
export function generateToken(payload: { id: string; loginId: string; name: string; email: string; roles: string[] }): string {
|
|
471
|
+
return jwt.sign(
|
|
472
|
+
{ sub: payload.id, loginId: payload.loginId, name: payload.name, email: payload.email, roles: payload.roles },
|
|
473
|
+
JWT_SECRET,
|
|
474
|
+
{ expiresIn: process.env.JWT_EXPIRY || '24h' } as jwt.SignOptions
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 認証エラー
|
|
480
|
+
*/
|
|
481
|
+
export class AuthError extends Error {
|
|
482
|
+
constructor(public statusCode: number, message: string) {
|
|
483
|
+
super(message);
|
|
484
|
+
this.name = 'AuthError';
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* AuthError を HTTP レスポンスに変換
|
|
490
|
+
*/
|
|
491
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null {
|
|
492
|
+
if (error instanceof AuthError) {
|
|
493
|
+
return { status: error.statusCode, jsonBody: { error: error.message } };
|
|
494
|
+
}
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
// ============================================================
|
|
500
|
+
// 4. C# Auth Functions テンプレート
|
|
501
|
+
// ============================================================
|
|
502
|
+
function generateAuthFunctionsCSharp(config, provider) {
|
|
503
|
+
const usingStatement = provider === "mysql"
|
|
504
|
+
? `using MySqlConnector;`
|
|
505
|
+
: provider === "postgres"
|
|
506
|
+
? `using Npgsql;`
|
|
507
|
+
: `using Microsoft.Data.SqlClient;`;
|
|
508
|
+
const connType = provider === "mysql"
|
|
509
|
+
? "MySqlConnection"
|
|
510
|
+
: provider === "postgres"
|
|
511
|
+
? "NpgsqlConnection"
|
|
512
|
+
: "SqlConnection";
|
|
513
|
+
const cmdType = provider === "mysql"
|
|
514
|
+
? "MySqlCommand"
|
|
515
|
+
: provider === "postgres"
|
|
516
|
+
? "NpgsqlCommand"
|
|
517
|
+
: "SqlCommand";
|
|
518
|
+
return `using System;
|
|
519
|
+
using System.Text.Json;
|
|
520
|
+
using Microsoft.Azure.Functions.Worker;
|
|
521
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
522
|
+
${usingStatement}
|
|
523
|
+
using BCrypt.Net;
|
|
524
|
+
|
|
525
|
+
namespace Functions.Auth
|
|
526
|
+
{
|
|
527
|
+
public class AuthFunctions
|
|
528
|
+
{
|
|
529
|
+
[Function("auth-login")]
|
|
530
|
+
public async Task<HttpResponseData> Login(
|
|
531
|
+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/login")] HttpRequestData request)
|
|
532
|
+
{
|
|
533
|
+
var body = await JsonSerializer.DeserializeAsync<LoginRequest>(request.Body);
|
|
534
|
+
if (body == null || string.IsNullOrEmpty(body.LoginId) || string.IsNullOrEmpty(body.Password))
|
|
535
|
+
{
|
|
536
|
+
var badReq = request.CreateResponse(System.Net.HttpStatusCode.BadRequest);
|
|
537
|
+
await badReq.WriteAsJsonAsync(new { error = "loginId and password are required" });
|
|
538
|
+
return badReq;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
await using var conn = new ${connType}(
|
|
542
|
+
Environment.GetEnvironmentVariable("${config.userConnector.toUpperCase()}_CONNECTION_STRING"));
|
|
543
|
+
await conn.OpenAsync();
|
|
544
|
+
|
|
545
|
+
await using var cmd = new ${cmdType}(
|
|
546
|
+
"SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = @loginId", conn);
|
|
547
|
+
cmd.Parameters.AddWithValue("@loginId", body.LoginId);
|
|
548
|
+
|
|
549
|
+
await using var reader = await cmd.ExecuteReaderAsync();
|
|
550
|
+
if (!await reader.ReadAsync())
|
|
551
|
+
{
|
|
552
|
+
var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
|
|
553
|
+
await unauthorized.WriteAsJsonAsync(new { error = "Invalid credentials" });
|
|
554
|
+
return unauthorized;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
var passwordHash = reader["${config.passwordHashColumn}"].ToString()!;
|
|
558
|
+
if (!BCrypt.Net.BCrypt.Verify(body.Password, passwordHash))
|
|
559
|
+
{
|
|
560
|
+
var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
|
|
561
|
+
await unauthorized.WriteAsJsonAsync(new { error = "Invalid credentials" });
|
|
562
|
+
return unauthorized;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
var rolesStr = reader["${config.rolesColumn}"].ToString() ?? "[]";
|
|
566
|
+
string[] roles;
|
|
567
|
+
try { roles = JsonSerializer.Deserialize<string[]>(rolesStr) ?? Array.Empty<string>(); }
|
|
568
|
+
catch { roles = rolesStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); }
|
|
569
|
+
|
|
570
|
+
var userId = reader["id"].ToString()!;
|
|
571
|
+
var loginId = reader["${config.loginIdColumn}"].ToString()!;
|
|
572
|
+
var name = reader["name"]?.ToString() ?? loginId;
|
|
573
|
+
var email = reader["email"]?.ToString() ?? "";
|
|
574
|
+
|
|
575
|
+
var token = JwtHelper.GenerateToken(userId, loginId, name, email, roles);
|
|
576
|
+
var expiresAt = DateTime.UtcNow.AddHours(24).ToString("o");
|
|
577
|
+
|
|
578
|
+
var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
|
|
579
|
+
await response.WriteAsJsonAsync(new
|
|
580
|
+
{
|
|
581
|
+
user = new { id = userId, loginId, name, email, roles },
|
|
582
|
+
token,
|
|
583
|
+
expiresAt
|
|
584
|
+
});
|
|
585
|
+
return response;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
[Function("auth-me")]
|
|
589
|
+
public async Task<HttpResponseData> Me(
|
|
590
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "auth/me")] HttpRequestData request)
|
|
591
|
+
{
|
|
592
|
+
var (principal, errorResponse) = await JwtHelper.Authorize(request);
|
|
593
|
+
if (errorResponse != null) return errorResponse;
|
|
594
|
+
|
|
595
|
+
var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
|
|
596
|
+
await response.WriteAsJsonAsync(new
|
|
597
|
+
{
|
|
598
|
+
sub = principal!.Sub,
|
|
599
|
+
loginId = principal.LoginId,
|
|
600
|
+
name = principal.Name,
|
|
601
|
+
email = principal.Email,
|
|
602
|
+
roles = principal.Roles
|
|
603
|
+
});
|
|
604
|
+
return response;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
[Function("auth-logout")]
|
|
608
|
+
public async Task<HttpResponseData> Logout(
|
|
609
|
+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/logout")] HttpRequestData request)
|
|
610
|
+
{
|
|
611
|
+
var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
|
|
612
|
+
await response.WriteAsJsonAsync(new { message = "Logged out" });
|
|
613
|
+
return response;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
public class LoginRequest
|
|
618
|
+
{
|
|
619
|
+
[System.Text.Json.Serialization.JsonPropertyName("loginId")]
|
|
620
|
+
public string LoginId { get; set; } = "";
|
|
621
|
+
[System.Text.Json.Serialization.JsonPropertyName("password")]
|
|
622
|
+
public string Password { get; set; } = "";
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
`;
|
|
626
|
+
}
|
|
627
|
+
// ============================================================
|
|
628
|
+
// 5. C# JWT Helper
|
|
629
|
+
// ============================================================
|
|
630
|
+
function generateJwtHelperCSharp() {
|
|
631
|
+
return `using System;
|
|
632
|
+
using System.IdentityModel.Tokens.Jwt;
|
|
633
|
+
using System.Security.Claims;
|
|
634
|
+
using System.Text;
|
|
635
|
+
using System.Text.Json;
|
|
636
|
+
using System.Threading.Tasks;
|
|
637
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
638
|
+
using Microsoft.IdentityModel.Tokens;
|
|
639
|
+
|
|
640
|
+
namespace Functions.Auth
|
|
641
|
+
{
|
|
642
|
+
public class JwtPayload
|
|
643
|
+
{
|
|
644
|
+
public string Sub { get; set; } = "";
|
|
645
|
+
public string LoginId { get; set; } = "";
|
|
646
|
+
public string Name { get; set; } = "";
|
|
647
|
+
public string Email { get; set; } = "";
|
|
648
|
+
public string[] Roles { get; set; } = Array.Empty<string>();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
public static class JwtHelper
|
|
652
|
+
{
|
|
653
|
+
private static string JwtSecret => Environment.GetEnvironmentVariable("JWT_SECRET") ?? "";
|
|
654
|
+
|
|
655
|
+
public static string GenerateToken(string userId, string loginId, string name, string email, string[] roles)
|
|
656
|
+
{
|
|
657
|
+
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret));
|
|
658
|
+
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
|
659
|
+
|
|
660
|
+
var claims = new List<Claim>
|
|
661
|
+
{
|
|
662
|
+
new(JwtRegisteredClaimNames.Sub, userId),
|
|
663
|
+
new("loginId", loginId),
|
|
664
|
+
new("name", name),
|
|
665
|
+
new("email", email),
|
|
666
|
+
};
|
|
667
|
+
foreach (var role in roles)
|
|
668
|
+
{
|
|
669
|
+
claims.Add(new Claim("roles", role));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
var token = new JwtSecurityToken(
|
|
673
|
+
expires: DateTime.UtcNow.AddHours(24),
|
|
674
|
+
claims: claims,
|
|
675
|
+
signingCredentials: creds
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
return new JwtSecurityTokenHandler().WriteToken(token);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
public static JwtPayload? ValidateToken(HttpRequestData request)
|
|
682
|
+
{
|
|
683
|
+
var authHeader = request.Headers.TryGetValues("Authorization", out var values)
|
|
684
|
+
? values.FirstOrDefault() : null;
|
|
685
|
+
|
|
686
|
+
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
|
|
687
|
+
return null;
|
|
688
|
+
|
|
689
|
+
var token = authHeader["Bearer ".Length..];
|
|
690
|
+
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret));
|
|
691
|
+
|
|
692
|
+
try
|
|
693
|
+
{
|
|
694
|
+
var handler = new JwtSecurityTokenHandler();
|
|
695
|
+
var principal = handler.ValidateToken(token, new TokenValidationParameters
|
|
696
|
+
{
|
|
697
|
+
ValidateIssuer = false,
|
|
698
|
+
ValidateAudience = false,
|
|
699
|
+
ValidateLifetime = true,
|
|
700
|
+
IssuerSigningKey = key,
|
|
701
|
+
}, out _);
|
|
702
|
+
|
|
703
|
+
return new JwtPayload
|
|
704
|
+
{
|
|
705
|
+
Sub = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value ?? "",
|
|
706
|
+
LoginId = principal.FindFirst("loginId")?.Value ?? "",
|
|
707
|
+
Name = principal.FindFirst("name")?.Value ?? "",
|
|
708
|
+
Email = principal.FindFirst("email")?.Value ?? "",
|
|
709
|
+
Roles = principal.FindAll("roles").Select(c => c.Value).ToArray(),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
catch
|
|
713
|
+
{
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
public static async Task<(JwtPayload?, HttpResponseData?)> Authorize(
|
|
719
|
+
HttpRequestData request, params string[] requiredRoles)
|
|
720
|
+
{
|
|
721
|
+
var payload = ValidateToken(request);
|
|
722
|
+
if (payload == null)
|
|
723
|
+
{
|
|
724
|
+
var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
|
|
725
|
+
await unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" });
|
|
726
|
+
return (null, unauthorized);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (requiredRoles.Length > 0 && !requiredRoles.Any(r => payload.Roles.Contains(r)))
|
|
730
|
+
{
|
|
731
|
+
var forbidden = request.CreateResponse(System.Net.HttpStatusCode.Forbidden);
|
|
732
|
+
await forbidden.WriteAsJsonAsync(new { error = "Forbidden" });
|
|
733
|
+
return (null, forbidden);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return (payload, null);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
`;
|
|
741
|
+
}
|
|
742
|
+
// ============================================================
|
|
743
|
+
// 6. Python Auth Functions テンプレート
|
|
744
|
+
// ============================================================
|
|
745
|
+
function generateAuthFunctionsPython(config, provider) {
|
|
746
|
+
const envPrefix = config.userConnector.toUpperCase();
|
|
747
|
+
const driverImport = provider === "mysql"
|
|
748
|
+
? `import mysql.connector`
|
|
749
|
+
: provider === "postgres"
|
|
750
|
+
? `import psycopg2\nimport psycopg2.extras`
|
|
751
|
+
: `import pymssql`;
|
|
752
|
+
const getConnection = provider === "mysql"
|
|
753
|
+
? `def get_connection():
|
|
754
|
+
return mysql.connector.connect(
|
|
755
|
+
host=os.environ.get("${envPrefix}_HOST", "localhost"),
|
|
756
|
+
user=os.environ.get("${envPrefix}_USER", "root"),
|
|
757
|
+
password=os.environ.get("${envPrefix}_PASSWORD", ""),
|
|
758
|
+
database=os.environ.get("${envPrefix}_DATABASE", ""),
|
|
759
|
+
)`
|
|
760
|
+
: provider === "postgres"
|
|
761
|
+
? `def get_connection():
|
|
762
|
+
return psycopg2.connect(os.environ.get("${envPrefix}_CONNECTION_STRING", ""))`
|
|
763
|
+
: `def get_connection():
|
|
764
|
+
return pymssql.connect(
|
|
765
|
+
server=os.environ.get("${envPrefix}_SERVER", "localhost"),
|
|
766
|
+
user=os.environ.get("${envPrefix}_USER", ""),
|
|
767
|
+
password=os.environ.get("${envPrefix}_PASSWORD", ""),
|
|
768
|
+
database=os.environ.get("${envPrefix}_DATABASE", ""),
|
|
769
|
+
)`;
|
|
770
|
+
const cursorCreate = provider === "mysql"
|
|
771
|
+
? `cursor = conn.cursor(dictionary=True)`
|
|
772
|
+
: provider === "postgres"
|
|
773
|
+
? `cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)`
|
|
774
|
+
: `cursor = conn.cursor(as_dict=True)`;
|
|
775
|
+
return `import json
|
|
776
|
+
import os
|
|
777
|
+
import logging
|
|
778
|
+
import azure.functions as func
|
|
779
|
+
import jwt
|
|
780
|
+
import bcrypt
|
|
781
|
+
${driverImport}
|
|
782
|
+
|
|
783
|
+
auth_bp = func.Blueprint()
|
|
784
|
+
|
|
785
|
+
${getConnection}
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
@auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
|
|
789
|
+
def auth_login(req: func.HttpRequest) -> func.HttpResponse:
|
|
790
|
+
try:
|
|
791
|
+
body = req.get_json()
|
|
792
|
+
login_id = body.get("loginId", "")
|
|
793
|
+
password = body.get("password", "")
|
|
794
|
+
|
|
795
|
+
if not login_id or not password:
|
|
796
|
+
return func.HttpResponse(
|
|
797
|
+
json.dumps({"error": "loginId and password are required"}),
|
|
798
|
+
status_code=400, mimetype="application/json"
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
conn = get_connection()
|
|
802
|
+
${cursorCreate}
|
|
803
|
+
cursor.execute(
|
|
804
|
+
"SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = %s",
|
|
805
|
+
(login_id,)
|
|
806
|
+
)
|
|
807
|
+
user = cursor.fetchone()
|
|
808
|
+
cursor.close()
|
|
809
|
+
conn.close()
|
|
810
|
+
|
|
811
|
+
if not user:
|
|
812
|
+
return func.HttpResponse(
|
|
813
|
+
json.dumps({"error": "Invalid credentials"}),
|
|
814
|
+
status_code=401, mimetype="application/json"
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
if not bcrypt.checkpw(
|
|
818
|
+
password.encode("utf-8"),
|
|
819
|
+
user["${config.passwordHashColumn}"].encode("utf-8")
|
|
820
|
+
):
|
|
821
|
+
return func.HttpResponse(
|
|
822
|
+
json.dumps({"error": "Invalid credentials"}),
|
|
823
|
+
status_code=401, mimetype="application/json"
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
roles_raw = user.get("${config.rolesColumn}", "[]")
|
|
827
|
+
try:
|
|
828
|
+
roles = json.loads(roles_raw) if isinstance(roles_raw, str) else list(roles_raw)
|
|
829
|
+
except (json.JSONDecodeError, TypeError):
|
|
830
|
+
roles = [r.strip() for r in str(roles_raw).split(",")]
|
|
831
|
+
|
|
832
|
+
from auth.jwt_helper import generate_token
|
|
833
|
+
import datetime
|
|
834
|
+
|
|
835
|
+
auth_user = {
|
|
836
|
+
"id": str(user["id"]),
|
|
837
|
+
"loginId": user["${config.loginIdColumn}"],
|
|
838
|
+
"name": user.get("name", user["${config.loginIdColumn}"]),
|
|
839
|
+
"email": user.get("email", ""),
|
|
840
|
+
"roles": roles,
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
token = generate_token(auth_user)
|
|
844
|
+
expires_at = (
|
|
845
|
+
datetime.datetime.utcnow() + datetime.timedelta(hours=24)
|
|
846
|
+
).isoformat() + "Z"
|
|
847
|
+
|
|
848
|
+
return func.HttpResponse(
|
|
849
|
+
json.dumps({"user": auth_user, "token": token, "expiresAt": expires_at}),
|
|
850
|
+
status_code=200, mimetype="application/json"
|
|
851
|
+
)
|
|
852
|
+
except Exception as e:
|
|
853
|
+
logging.error(f"Login error: {e}")
|
|
854
|
+
return func.HttpResponse(
|
|
855
|
+
json.dumps({"error": "Login failed"}),
|
|
856
|
+
status_code=500, mimetype="application/json"
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
@auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.FUNCTION)
|
|
861
|
+
def auth_me(req: func.HttpRequest) -> func.HttpResponse:
|
|
862
|
+
from auth.jwt_helper import require_auth, handle_auth_error
|
|
863
|
+
|
|
864
|
+
try:
|
|
865
|
+
user = require_auth(req)
|
|
866
|
+
return func.HttpResponse(
|
|
867
|
+
json.dumps(user), status_code=200, mimetype="application/json"
|
|
868
|
+
)
|
|
869
|
+
except Exception as e:
|
|
870
|
+
err = handle_auth_error(e)
|
|
871
|
+
if err:
|
|
872
|
+
return err
|
|
873
|
+
return func.HttpResponse(
|
|
874
|
+
json.dumps({"error": "Internal server error"}),
|
|
875
|
+
status_code=500, mimetype="application/json"
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
@auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
|
|
880
|
+
def auth_logout(req: func.HttpRequest) -> func.HttpResponse:
|
|
881
|
+
return func.HttpResponse(
|
|
882
|
+
json.dumps({"message": "Logged out"}),
|
|
883
|
+
status_code=200, mimetype="application/json"
|
|
884
|
+
)
|
|
885
|
+
`;
|
|
886
|
+
}
|
|
887
|
+
// ============================================================
|
|
888
|
+
// 7. Python JWT Helper
|
|
889
|
+
// ============================================================
|
|
890
|
+
function generateJwtHelperPython() {
|
|
891
|
+
return `import os
|
|
892
|
+
import json
|
|
893
|
+
import jwt
|
|
894
|
+
import azure.functions as func
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
JWT_SECRET = os.environ.get("JWT_SECRET", "")
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def require_auth(req: func.HttpRequest) -> dict:
|
|
901
|
+
"""JWT 検証。無効な場合は AuthError を raise。"""
|
|
902
|
+
auth_header = req.headers.get("authorization", "")
|
|
903
|
+
if not auth_header.startswith("Bearer "):
|
|
904
|
+
raise AuthError(401, "Missing or invalid Authorization header")
|
|
905
|
+
|
|
906
|
+
token = auth_header[7:]
|
|
907
|
+
try:
|
|
908
|
+
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
|
909
|
+
return {
|
|
910
|
+
"sub": payload.get("sub", ""),
|
|
911
|
+
"loginId": payload.get("loginId", ""),
|
|
912
|
+
"name": payload.get("name", ""),
|
|
913
|
+
"email": payload.get("email", ""),
|
|
914
|
+
"roles": payload.get("roles", []),
|
|
915
|
+
}
|
|
916
|
+
except jwt.ExpiredSignatureError:
|
|
917
|
+
raise AuthError(401, "Token expired")
|
|
918
|
+
except jwt.InvalidTokenError:
|
|
919
|
+
raise AuthError(401, "Invalid token")
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def require_roles(user: dict, required_roles: list[str]) -> None:
|
|
923
|
+
"""ロール確認。必要なロールを持たない場合は AuthError を raise。"""
|
|
924
|
+
user_roles = user.get("roles", [])
|
|
925
|
+
if not any(role in user_roles for role in required_roles):
|
|
926
|
+
raise AuthError(403, f"Requires one of roles: {', '.join(required_roles)}")
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def generate_token(payload: dict) -> str:
|
|
930
|
+
"""JWT 生成"""
|
|
931
|
+
import datetime
|
|
932
|
+
|
|
933
|
+
return jwt.encode(
|
|
934
|
+
{
|
|
935
|
+
"sub": payload["id"],
|
|
936
|
+
"loginId": payload["loginId"],
|
|
937
|
+
"name": payload["name"],
|
|
938
|
+
"email": payload["email"],
|
|
939
|
+
"roles": payload["roles"],
|
|
940
|
+
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24),
|
|
941
|
+
},
|
|
942
|
+
JWT_SECRET,
|
|
943
|
+
algorithm="HS256",
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
class AuthError(Exception):
|
|
948
|
+
def __init__(self, status_code: int, message: str):
|
|
949
|
+
super().__init__(message)
|
|
950
|
+
self.status_code = status_code
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def handle_auth_error(error: Exception) -> func.HttpResponse | None:
|
|
954
|
+
"""AuthError を HTTP レスポンスに変換"""
|
|
955
|
+
if isinstance(error, AuthError):
|
|
956
|
+
return func.HttpResponse(
|
|
957
|
+
json.dumps({"error": str(error)}),
|
|
958
|
+
status_code=error.status_code,
|
|
959
|
+
mimetype="application/json",
|
|
960
|
+
)
|
|
961
|
+
return None
|
|
962
|
+
`;
|
|
963
|
+
}
|
|
964
|
+
// ============================================================
|
|
965
|
+
// 8. BFF Auth Routes(Next.js API Routes)
|
|
966
|
+
// ============================================================
|
|
967
|
+
function generateBFFAuthLoginRoute(projectName, sharedPackageName) {
|
|
968
|
+
const cookieName = projectName.replace(/^@[^/]+\//, '').replace(/[^a-z0-9-]/g, '-') + '-auth-token';
|
|
969
|
+
return `import { NextRequest, NextResponse } from 'next/server';
|
|
970
|
+
import { LoginRequest, LoginResponse } from '${sharedPackageName}';
|
|
971
|
+
|
|
972
|
+
export async function POST(request: NextRequest) {
|
|
973
|
+
const FUNCTIONS_BASE_URL = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
974
|
+
try {
|
|
975
|
+
const body = await request.json();
|
|
976
|
+
const validated = LoginRequest.parse(body);
|
|
977
|
+
|
|
978
|
+
const result = await fetch(\`\${FUNCTIONS_BASE_URL}/api/auth/login\`, {
|
|
979
|
+
method: 'POST',
|
|
980
|
+
headers: { 'Content-Type': 'application/json' },
|
|
981
|
+
body: JSON.stringify(validated),
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
if (!result.ok) {
|
|
985
|
+
const error = await result.json().catch(() => ({ error: 'Authentication failed' }));
|
|
986
|
+
return NextResponse.json(error, { status: result.status });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const data = LoginResponse.parse(await result.json());
|
|
990
|
+
|
|
991
|
+
const response = NextResponse.json({ user: data.user });
|
|
992
|
+
response.cookies.set('${cookieName}', data.token, {
|
|
993
|
+
httpOnly: true,
|
|
994
|
+
secure: process.env.NODE_ENV === 'production',
|
|
995
|
+
sameSite: 'lax',
|
|
996
|
+
path: '/',
|
|
997
|
+
maxAge: 86400,
|
|
998
|
+
});
|
|
999
|
+
return response;
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
console.error('[BFF] Login error:', error);
|
|
1002
|
+
return NextResponse.json({ error: 'Login failed' }, { status: 500 });
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
`;
|
|
1006
|
+
}
|
|
1007
|
+
function generateBFFAuthLogoutRoute(projectName) {
|
|
1008
|
+
const cookieName = projectName.replace(/^@[^/]+\//, '').replace(/[^a-z0-9-]/g, '-') + '-auth-token';
|
|
1009
|
+
return `import { NextResponse } from 'next/server';
|
|
1010
|
+
|
|
1011
|
+
export async function POST() {
|
|
1012
|
+
const response = NextResponse.json({ message: 'Logged out' });
|
|
1013
|
+
response.cookies.delete('${cookieName}');
|
|
1014
|
+
return response;
|
|
1015
|
+
}
|
|
1016
|
+
`;
|
|
1017
|
+
}
|
|
1018
|
+
function generateBFFAuthMeRoute(_sharedPackageName) {
|
|
1019
|
+
void _sharedPackageName;
|
|
1020
|
+
return `import { NextResponse } from 'next/server';
|
|
1021
|
+
import { headers } from 'next/headers';
|
|
1022
|
+
|
|
1023
|
+
export async function GET() {
|
|
1024
|
+
const FUNCTIONS_BASE_URL = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
1025
|
+
try {
|
|
1026
|
+
const reqHeaders = await headers();
|
|
1027
|
+
const authorization = reqHeaders.get('authorization');
|
|
1028
|
+
|
|
1029
|
+
if (!authorization) {
|
|
1030
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const result = await fetch(\`\${FUNCTIONS_BASE_URL}/api/auth/me\`, {
|
|
1034
|
+
headers: { Authorization: authorization },
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
if (!result.ok) {
|
|
1038
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: result.status });
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const data = await result.json();
|
|
1042
|
+
return NextResponse.json(data);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
console.error('[BFF] Auth/me error:', error);
|
|
1045
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
`;
|
|
1049
|
+
}
|
|
1050
|
+
// ============================================================
|
|
1051
|
+
// 9. Next.js Proxy (formerly Middleware)
|
|
1052
|
+
// ============================================================
|
|
1053
|
+
function generateProxy(projectName) {
|
|
1054
|
+
const cookieName = projectName.replace(/^@[^/]+\//, '').replace(/[^a-z0-9-]/g, '-') + '-auth-token';
|
|
1055
|
+
return `import { NextResponse } from 'next/server';
|
|
1056
|
+
import type { NextRequest } from 'next/server';
|
|
1057
|
+
|
|
1058
|
+
const AUTH_COOKIE_NAME = '${cookieName}';
|
|
1059
|
+
const PUBLIC_PATHS = ['/login', '/api/auth/login', '/api/auth/logout'];
|
|
1060
|
+
|
|
1061
|
+
export function proxy(request: NextRequest) {
|
|
1062
|
+
const { pathname } = request.nextUrl;
|
|
1063
|
+
|
|
1064
|
+
// 公開パス・静的アセットはスキップ
|
|
1065
|
+
if (PUBLIC_PATHS.some(p => pathname.startsWith(p))) return NextResponse.next();
|
|
1066
|
+
if (pathname.startsWith('/_next') || pathname.match(/\\.(ico|png|jpg|svg|css|js)$/)) {
|
|
1067
|
+
return NextResponse.next();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const token = request.cookies.get(AUTH_COOKIE_NAME)?.value;
|
|
1071
|
+
|
|
1072
|
+
if (!token) {
|
|
1073
|
+
if (pathname.startsWith('/api/')) {
|
|
1074
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
1075
|
+
}
|
|
1076
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// JWT 有効期限の簡易チェック(署名検証なし)
|
|
1080
|
+
try {
|
|
1081
|
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
1082
|
+
if (payload.exp && payload.exp * 1000 < Date.now()) {
|
|
1083
|
+
const response = pathname.startsWith('/api/')
|
|
1084
|
+
? NextResponse.json({ error: 'Token expired' }, { status: 401 })
|
|
1085
|
+
: NextResponse.redirect(new URL('/login', request.url));
|
|
1086
|
+
response.cookies.delete(AUTH_COOKIE_NAME);
|
|
1087
|
+
return response;
|
|
1088
|
+
}
|
|
1089
|
+
} catch {
|
|
1090
|
+
const response = pathname.startsWith('/api/')
|
|
1091
|
+
? NextResponse.json({ error: 'Invalid token' }, { status: 401 })
|
|
1092
|
+
: NextResponse.redirect(new URL('/login', request.url));
|
|
1093
|
+
response.cookies.delete(AUTH_COOKIE_NAME);
|
|
1094
|
+
return response;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Authorization ヘッダーを注入(BFF callFunction が自動転送できるように)
|
|
1098
|
+
const requestHeaders = new Headers(request.headers);
|
|
1099
|
+
requestHeaders.set('Authorization', \`Bearer \${token}\`);
|
|
1100
|
+
return NextResponse.next({
|
|
1101
|
+
request: { headers: requestHeaders },
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
`;
|
|
1105
|
+
}
|
|
1106
|
+
// ============================================================
|
|
1107
|
+
// 10. ログインページ
|
|
1108
|
+
// ============================================================
|
|
1109
|
+
function generateLoginPage() {
|
|
1110
|
+
return `'use client';
|
|
1111
|
+
|
|
1112
|
+
import { useState } from 'react';
|
|
1113
|
+
import { useRouter } from 'next/navigation';
|
|
1114
|
+
|
|
1115
|
+
export default function LoginPage() {
|
|
1116
|
+
const [loginId, setLoginId] = useState('');
|
|
1117
|
+
const [password, setPassword] = useState('');
|
|
1118
|
+
const [error, setError] = useState('');
|
|
1119
|
+
const [loading, setLoading] = useState(false);
|
|
1120
|
+
const router = useRouter();
|
|
1121
|
+
|
|
1122
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
1123
|
+
e.preventDefault();
|
|
1124
|
+
setError('');
|
|
1125
|
+
setLoading(true);
|
|
1126
|
+
|
|
1127
|
+
try {
|
|
1128
|
+
const res = await fetch('/api/auth/login', {
|
|
1129
|
+
method: 'POST',
|
|
1130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1131
|
+
body: JSON.stringify({ loginId, password }),
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
if (!res.ok) {
|
|
1135
|
+
const data = await res.json().catch(() => ({}));
|
|
1136
|
+
setError(data.error || 'ログインに失敗しました');
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
router.push('/');
|
|
1141
|
+
router.refresh();
|
|
1142
|
+
} catch {
|
|
1143
|
+
setError('通信エラーが発生しました');
|
|
1144
|
+
} finally {
|
|
1145
|
+
setLoading(false);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
return (
|
|
1150
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
1151
|
+
<div className="w-full max-w-md p-8 bg-white rounded-lg shadow-md">
|
|
1152
|
+
<h1 className="text-2xl font-bold text-center mb-6">ログイン</h1>
|
|
1153
|
+
{error && (
|
|
1154
|
+
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded text-sm">
|
|
1155
|
+
{error}
|
|
1156
|
+
</div>
|
|
1157
|
+
)}
|
|
1158
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
1159
|
+
<div>
|
|
1160
|
+
<label htmlFor="loginId" className="block text-sm font-medium text-gray-700 mb-1">
|
|
1161
|
+
ログインID
|
|
1162
|
+
</label>
|
|
1163
|
+
<input
|
|
1164
|
+
id="loginId"
|
|
1165
|
+
type="text"
|
|
1166
|
+
value={loginId}
|
|
1167
|
+
onChange={(e) => setLoginId(e.target.value)}
|
|
1168
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
1169
|
+
required
|
|
1170
|
+
autoFocus
|
|
1171
|
+
/>
|
|
1172
|
+
</div>
|
|
1173
|
+
<div>
|
|
1174
|
+
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
|
1175
|
+
パスワード
|
|
1176
|
+
</label>
|
|
1177
|
+
<input
|
|
1178
|
+
id="password"
|
|
1179
|
+
type="password"
|
|
1180
|
+
value={password}
|
|
1181
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
1182
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
1183
|
+
required
|
|
1184
|
+
/>
|
|
1185
|
+
</div>
|
|
1186
|
+
<button
|
|
1187
|
+
type="submit"
|
|
1188
|
+
disabled={loading}
|
|
1189
|
+
className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
1190
|
+
>
|
|
1191
|
+
{loading ? 'ログイン中...' : 'ログイン'}
|
|
1192
|
+
</button>
|
|
1193
|
+
</form>
|
|
1194
|
+
</div>
|
|
1195
|
+
</div>
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
`;
|
|
1199
|
+
}
|
|
1200
|
+
// ============================================================
|
|
1201
|
+
// 11. React Auth Context
|
|
1202
|
+
// ============================================================
|
|
1203
|
+
function generateAuthContext() {
|
|
1204
|
+
return `'use client';
|
|
1205
|
+
|
|
1206
|
+
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
|
1207
|
+
import { useRouter } from 'next/navigation';
|
|
1208
|
+
|
|
1209
|
+
interface AuthUser {
|
|
1210
|
+
id: string;
|
|
1211
|
+
loginId: string;
|
|
1212
|
+
name: string;
|
|
1213
|
+
email: string;
|
|
1214
|
+
roles: string[];
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
interface AuthContextType {
|
|
1218
|
+
user: AuthUser | null;
|
|
1219
|
+
loading: boolean;
|
|
1220
|
+
login: (loginId: string, password: string) => Promise<{ success: boolean; error?: string }>;
|
|
1221
|
+
logout: () => Promise<void>;
|
|
1222
|
+
hasRole: (role: string) => boolean;
|
|
1223
|
+
hasAnyRole: (roles: string[]) => boolean;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
1227
|
+
|
|
1228
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
1229
|
+
const [user, setUser] = useState<AuthUser | null>(null);
|
|
1230
|
+
const [loading, setLoading] = useState(true);
|
|
1231
|
+
const router = useRouter();
|
|
1232
|
+
|
|
1233
|
+
useEffect(() => {
|
|
1234
|
+
let cancelled = false;
|
|
1235
|
+
fetch('/api/auth/me')
|
|
1236
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1237
|
+
.catch(() => null)
|
|
1238
|
+
.then((data) => {
|
|
1239
|
+
if (cancelled) return;
|
|
1240
|
+
setUser(data);
|
|
1241
|
+
setLoading(false);
|
|
1242
|
+
});
|
|
1243
|
+
return () => {
|
|
1244
|
+
cancelled = true;
|
|
1245
|
+
};
|
|
1246
|
+
}, []);
|
|
1247
|
+
|
|
1248
|
+
const login = async (loginId: string, password: string) => {
|
|
1249
|
+
try {
|
|
1250
|
+
const res = await fetch('/api/auth/login', {
|
|
1251
|
+
method: 'POST',
|
|
1252
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1253
|
+
body: JSON.stringify({ loginId, password }),
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
if (!res.ok) {
|
|
1257
|
+
const data = await res.json().catch(() => ({}));
|
|
1258
|
+
return { success: false, error: data.error || 'Login failed' };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const data = await res.json();
|
|
1262
|
+
setUser(data.user);
|
|
1263
|
+
return { success: true };
|
|
1264
|
+
} catch {
|
|
1265
|
+
return { success: false, error: 'Network error' };
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
const logout = async () => {
|
|
1270
|
+
await fetch('/api/auth/logout', { method: 'POST' });
|
|
1271
|
+
setUser(null);
|
|
1272
|
+
router.push('/login');
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
const hasRole = (role: string) => user?.roles?.includes(role) ?? false;
|
|
1276
|
+
const hasAnyRole = (roles: string[]) => roles.some(r => hasRole(r));
|
|
1277
|
+
|
|
1278
|
+
return (
|
|
1279
|
+
<AuthContext.Provider value={{ user, loading, login, logout, hasRole, hasAnyRole }}>
|
|
1280
|
+
{children}
|
|
1281
|
+
</AuthContext.Provider>
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
export function useAuth() {
|
|
1286
|
+
const context = useContext(AuthContext);
|
|
1287
|
+
if (!context) {
|
|
1288
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
1289
|
+
}
|
|
1290
|
+
return context;
|
|
1291
|
+
}
|
|
1292
|
+
`;
|
|
1293
|
+
}
|
|
1294
|
+
// ============================================================
|
|
1295
|
+
// 12. callFunction 認証対応版
|
|
1296
|
+
// ============================================================
|
|
1297
|
+
function generateBFFCallFunctionWithAuth() {
|
|
1298
|
+
return `import { NextResponse } from 'next/server';
|
|
1299
|
+
import { headers } from 'next/headers';
|
|
1300
|
+
import { z } from 'zod/v4';
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* SwallowKit BFF Call Function Helper (Auth-enabled)
|
|
1304
|
+
* Azure Functions を呼び出す汎用ヘルパー(Authorization ヘッダー自動転送対応)
|
|
1305
|
+
*/
|
|
1306
|
+
|
|
1307
|
+
function getFunctionsBaseUrl(): string {
|
|
1308
|
+
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function isLocalFunctionsUrl(url: string): boolean {
|
|
1312
|
+
return url.startsWith('http://localhost:') || url.startsWith('http://127.0.0.1:');
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
1316
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
1317
|
+
if (key) return { 'x-functions-key': key };
|
|
1318
|
+
if (isLocalFunctionsUrl(functionsBaseUrl)) return {};
|
|
1319
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
interface CallFunctionConfig<TInput = unknown, TOutput = unknown> {
|
|
1323
|
+
/** HTTP メソッド */
|
|
1324
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
1325
|
+
/** Azure Functions のパス (例: '/api/todo', '/api/todo/123') */
|
|
1326
|
+
path: string;
|
|
1327
|
+
/** リクエストボディ (POST/PUT 用) */
|
|
1328
|
+
body?: unknown;
|
|
1329
|
+
/** 入力バリデーション用 Zod スキーマ (省略時はバリデーションなし) */
|
|
1330
|
+
inputSchema?: z.ZodSchema<TInput>;
|
|
1331
|
+
/** 出力バリデーション用 Zod スキーマ (省略時はそのまま返す) */
|
|
1332
|
+
responseSchema?: z.ZodSchema<TOutput>;
|
|
1333
|
+
/** 成功時の HTTP ステータスコード (デフォルト: 200) */
|
|
1334
|
+
successStatus?: number;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
export async function callFunction<TInput = unknown, TOutput = unknown>(
|
|
1338
|
+
config: CallFunctionConfig<TInput, TOutput>
|
|
1339
|
+
): Promise<NextResponse> {
|
|
1340
|
+
const { method, path, body, inputSchema, responseSchema, successStatus = 200 } = config;
|
|
1341
|
+
|
|
1342
|
+
try {
|
|
1343
|
+
// 入力バリデーション
|
|
1344
|
+
let validatedBody = body;
|
|
1345
|
+
if (inputSchema && body !== undefined) {
|
|
1346
|
+
const result = inputSchema.safeParse(body);
|
|
1347
|
+
if (!result.success) {
|
|
1348
|
+
console.error('[BFF] Validation failed:', result.error.issues);
|
|
1349
|
+
return NextResponse.json(
|
|
1350
|
+
{ error: 'Validation failed', details: result.error.issues },
|
|
1351
|
+
{ status: 400 }
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
validatedBody = result.data;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Azure Functions を呼び出し
|
|
1358
|
+
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
1359
|
+
const url = functionsBaseUrl + path;
|
|
1360
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
1361
|
+
console.log(\`[BFF] \${method} \${url}\`);
|
|
1362
|
+
|
|
1363
|
+
// Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)
|
|
1364
|
+
const fetchHeaders: Record<string, string> = {
|
|
1365
|
+
'Content-Type': 'application/json',
|
|
1366
|
+
...keyHeaders,
|
|
1367
|
+
};
|
|
1368
|
+
try {
|
|
1369
|
+
const reqHeaders = await headers();
|
|
1370
|
+
const authorization = reqHeaders.get('authorization');
|
|
1371
|
+
if (authorization) {
|
|
1372
|
+
fetchHeaders['Authorization'] = authorization;
|
|
1373
|
+
}
|
|
1374
|
+
} catch {
|
|
1375
|
+
// headers() が使えないコンテキスト(ISR 等)では無視
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const response = await fetch(url, {
|
|
1379
|
+
method,
|
|
1380
|
+
headers: fetchHeaders,
|
|
1381
|
+
body: validatedBody !== undefined ? JSON.stringify(validatedBody) : undefined,
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
console.log('[BFF] Functions response status:', response.status);
|
|
1385
|
+
|
|
1386
|
+
// エラーレスポンスの転送
|
|
1387
|
+
if (!response.ok) {
|
|
1388
|
+
const text = await response.text();
|
|
1389
|
+
console.error('[BFF] Functions error:', { status: response.status, body: text });
|
|
1390
|
+
|
|
1391
|
+
let errorMessage = 'Request failed';
|
|
1392
|
+
try {
|
|
1393
|
+
const error = JSON.parse(text);
|
|
1394
|
+
errorMessage = error.error || errorMessage;
|
|
1395
|
+
} catch {
|
|
1396
|
+
errorMessage = text || errorMessage;
|
|
1397
|
+
}
|
|
1398
|
+
return NextResponse.json({ error: errorMessage }, { status: response.status });
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// DELETE 204 の場合はボディなし
|
|
1402
|
+
if (response.status === 204 || method === 'DELETE') {
|
|
1403
|
+
return new NextResponse(null, { status: 204 });
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// レスポンスの取得と出力バリデーション
|
|
1407
|
+
const data = await response.json();
|
|
1408
|
+
|
|
1409
|
+
if (responseSchema) {
|
|
1410
|
+
const validated = responseSchema.parse(data);
|
|
1411
|
+
return NextResponse.json(validated, { status: successStatus });
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
return NextResponse.json(data, { status: successStatus });
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
console.error(\`[BFF] Error:\`, error);
|
|
1417
|
+
return NextResponse.json(
|
|
1418
|
+
{ error: error instanceof Error ? error.message : 'Internal server error' },
|
|
1419
|
+
{ status: 500 }
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
`;
|
|
1424
|
+
}
|
|
1425
|
+
function generateBFFSwaCredentialForwarding(includeBearer) {
|
|
1426
|
+
const bearerForwarding = includeBearer
|
|
1427
|
+
? ` const authorization = reqHeaders.get('authorization');
|
|
1428
|
+
if (authorization?.startsWith('Bearer ')) {
|
|
1429
|
+
fetchHeaders.Authorization = authorization;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
`
|
|
1433
|
+
: "";
|
|
1434
|
+
return `${bearerForwarding} let principal = reqHeaders.get('x-ms-client-principal');
|
|
1435
|
+
|
|
1436
|
+
if (!principal) {
|
|
1437
|
+
const cookie = reqHeaders.get('cookie');
|
|
1438
|
+
const forwardedHost = reqHeaders.get('x-forwarded-host');
|
|
1439
|
+
const requestHost = reqHeaders.get('host');
|
|
1440
|
+
const host = (forwardedHost || requestHost)?.split(',')[0].trim();
|
|
1441
|
+
|
|
1442
|
+
if (cookie && host) {
|
|
1443
|
+
const forwardedProto = reqHeaders.get('x-forwarded-proto');
|
|
1444
|
+
const proto =
|
|
1445
|
+
forwardedProto?.split(',')[0].trim() ||
|
|
1446
|
+
(isLocalFunctionsUrl(functionsBaseUrl) ? 'http' : 'https');
|
|
1447
|
+
|
|
1448
|
+
const meResponse = await fetch(\`\${proto}://\${host}/.auth/me\`, {
|
|
1449
|
+
headers: { cookie },
|
|
1450
|
+
cache: 'no-store',
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
if (meResponse.ok) {
|
|
1454
|
+
const payload = (await meResponse.json()) as {
|
|
1455
|
+
clientPrincipal?: unknown;
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
if (payload.clientPrincipal) {
|
|
1459
|
+
principal = Buffer.from(
|
|
1460
|
+
JSON.stringify(payload.clientPrincipal),
|
|
1461
|
+
'utf8'
|
|
1462
|
+
).toString('base64');
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (principal) {
|
|
1469
|
+
const decodedPrincipal = Buffer.from(principal, 'base64');
|
|
1470
|
+
if (decodedPrincipal.toString('base64') !== principal) {
|
|
1471
|
+
throw new Error('Invalid SWA client principal encoding');
|
|
1472
|
+
}
|
|
1473
|
+
const parsed = JSON.parse(
|
|
1474
|
+
decodedPrincipal.toString('utf8')
|
|
1475
|
+
) as Record<string, unknown>;
|
|
1476
|
+
|
|
1477
|
+
const normalized = {
|
|
1478
|
+
identityProvider:
|
|
1479
|
+
typeof parsed.identityProvider === 'string'
|
|
1480
|
+
? parsed.identityProvider
|
|
1481
|
+
: '',
|
|
1482
|
+
userId: typeof parsed.userId === 'string' ? parsed.userId : '',
|
|
1483
|
+
userDetails:
|
|
1484
|
+
typeof parsed.userDetails === 'string'
|
|
1485
|
+
? parsed.userDetails
|
|
1486
|
+
: '',
|
|
1487
|
+
userRoles: Array.isArray(parsed.userRoles)
|
|
1488
|
+
? parsed.userRoles.filter(
|
|
1489
|
+
(role: unknown): role is string => typeof role === 'string'
|
|
1490
|
+
)
|
|
1491
|
+
: [],
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
if (
|
|
1495
|
+
normalized.identityProvider &&
|
|
1496
|
+
normalized.userId &&
|
|
1497
|
+
normalized.userRoles.includes('authenticated')
|
|
1498
|
+
) {
|
|
1499
|
+
fetchHeaders['x-ms-client-principal'] = Buffer.from(
|
|
1500
|
+
JSON.stringify(normalized),
|
|
1501
|
+
'utf8'
|
|
1502
|
+
).toString('base64');
|
|
1503
|
+
}
|
|
1504
|
+
}`;
|
|
1505
|
+
}
|
|
1506
|
+
function generateBFFCallFunctionWithSwaCredentials(includeBearer) {
|
|
1507
|
+
return generateBFFCallFunctionWithAuth()
|
|
1508
|
+
.replace(` // Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)`, ` // Forward only validated credentials from the current request.`)
|
|
1509
|
+
.replace(` const authorization = reqHeaders.get('authorization');
|
|
1510
|
+
if (authorization) {
|
|
1511
|
+
fetchHeaders['Authorization'] = authorization;
|
|
1512
|
+
}`, generateBFFSwaCredentialForwarding(includeBearer))
|
|
1513
|
+
.replace(` } catch {
|
|
1514
|
+
// headers() が使えないコンテキスト(ISR 等)では無視
|
|
1515
|
+
}`, ` } catch (error) {
|
|
1516
|
+
// Do not include credentials or request header values in authentication logs.
|
|
1517
|
+
const errorType = error instanceof Error ? error.name : 'UnknownError';
|
|
1518
|
+
console.error('[BFF] Authentication forwarding failed:', errorType);
|
|
1519
|
+
}`);
|
|
1520
|
+
}
|
|
1521
|
+
/** SWA built-in auth variant: forward only a validated SWA client principal. */
|
|
1522
|
+
function generateBFFCallFunctionWithSwaAuth() {
|
|
1523
|
+
return generateBFFCallFunctionWithSwaCredentials(false);
|
|
1524
|
+
}
|
|
1525
|
+
/** Combined transport for named schemes. Functions still performs all verification. */
|
|
1526
|
+
function generateBFFCallFunctionWithMultipleAuth() {
|
|
1527
|
+
return generateBFFCallFunctionWithSwaCredentials(true);
|
|
1528
|
+
}
|
|
1529
|
+
function generateSwaAuthHelperTS() {
|
|
1530
|
+
return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1531
|
+
|
|
1532
|
+
export interface AuthUser { id: string; loginId: string; roles: string[]; }
|
|
1533
|
+
export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
|
|
1534
|
+
|
|
1535
|
+
export function requireAuth(request: HttpRequest): AuthUser {
|
|
1536
|
+
const encoded = request.headers.get('x-ms-client-principal');
|
|
1537
|
+
if (!encoded) throw new AuthError(401, 'Authentication required');
|
|
1538
|
+
try {
|
|
1539
|
+
const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
|
|
1540
|
+
const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r: unknown) => typeof r === 'string') : [];
|
|
1541
|
+
if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
|
|
1542
|
+
return { id: value.userId, loginId: typeof value.userDetails === 'string' ? value.userDetails : '', roles };
|
|
1543
|
+
} catch { throw new AuthError(401, 'Invalid client principal'); }
|
|
1544
|
+
}
|
|
1545
|
+
export function requireRoles(user: AuthUser, roles: string[]): void {
|
|
1546
|
+
if (roles.length && !roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions');
|
|
1547
|
+
}
|
|
1548
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null {
|
|
1549
|
+
return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null;
|
|
1550
|
+
}
|
|
1551
|
+
`;
|
|
1552
|
+
}
|
|
1553
|
+
// ============================================================
|
|
1554
|
+
// External token authentication
|
|
1555
|
+
// ============================================================
|
|
1556
|
+
function generateExternalTokenAdapter() {
|
|
1557
|
+
return `/** Replace these functions with your external IdP SDK integration. */
|
|
1558
|
+
export async function getToken(): Promise<string | null> { return null; }
|
|
1559
|
+
export async function login(): Promise<void> { throw new Error('External token login adapter is not configured'); }
|
|
1560
|
+
export async function logout(): Promise<void> { throw new Error('External token logout adapter is not configured'); }
|
|
1561
|
+
`;
|
|
1562
|
+
}
|
|
1563
|
+
function generateAuthenticatedFetch() {
|
|
1564
|
+
return `import { getToken } from './external-token-adapter';
|
|
1565
|
+
|
|
1566
|
+
export async function authenticatedFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
|
|
1567
|
+
const token = await getToken();
|
|
1568
|
+
if (!token) return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'content-type': 'application/json' } });
|
|
1569
|
+
const headers = new Headers(init.headers);
|
|
1570
|
+
headers.set('Authorization', \`Bearer \${token}\`);
|
|
1571
|
+
return fetch(input, { ...init, headers });
|
|
1572
|
+
}
|
|
1573
|
+
`;
|
|
1574
|
+
}
|
|
1575
|
+
function generateExternalTokenBFFMeRoute() {
|
|
1576
|
+
return `import { callFunction } from '@/lib/api/call-function';
|
|
1577
|
+
export async function GET() { return callFunction({ method: 'GET', path: '/api/auth/me' }); }
|
|
1578
|
+
`;
|
|
1579
|
+
}
|
|
1580
|
+
function generateExternalTokenAuthContext() {
|
|
1581
|
+
return `'use client';
|
|
1582
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
|
1583
|
+
import { authenticatedFetch } from './authenticated-fetch';
|
|
1584
|
+
import * as adapter from './external-token-adapter';
|
|
1585
|
+
export interface AuthUser { id: string; loginId: string; name: string; email: string; roles: string[]; }
|
|
1586
|
+
interface Value { user: AuthUser | null; loading: boolean; login: () => Promise<void>; logout: () => Promise<void>; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; }
|
|
1587
|
+
const Context = createContext<Value | null>(null);
|
|
1588
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
1589
|
+
const [user, setUser] = useState<AuthUser | null>(null); const [loading, setLoading] = useState(true);
|
|
1590
|
+
useEffect(() => { authenticatedFetch('/api/auth/me').then(async r => {
|
|
1591
|
+
if (!r.ok || !(r.headers.get('content-type') || '').includes('application/json')) return null;
|
|
1592
|
+
return r.json() as Promise<AuthUser>;
|
|
1593
|
+
}).then(setUser).catch(() => setUser(null)).finally(() => setLoading(false)); }, []);
|
|
1594
|
+
const hasRole = (role: string) => user?.roles.includes(role) ?? false;
|
|
1595
|
+
const logout = async () => { await adapter.logout(); setUser(null); };
|
|
1596
|
+
return <Context.Provider value={{ user, loading, login: adapter.login, logout, hasRole, hasAnyRole: roles => roles.some(hasRole) }}>{children}</Context.Provider>;
|
|
1597
|
+
}
|
|
1598
|
+
export function useAuth() { const value = useContext(Context); if (!value) throw new Error('useAuth must be used within an AuthProvider'); return value; }
|
|
1599
|
+
`;
|
|
1600
|
+
}
|
|
1601
|
+
function generateExternalTokenVerifierTS() {
|
|
1602
|
+
return `export interface ExternalAuthPrincipal { userId: string; userDetails: string; roles: string[]; }
|
|
1603
|
+
export class ExternalTokenVerificationError extends Error { constructor(public status: 401 | 503, message: string) { super(message); } }
|
|
1604
|
+
/** Implement IdP verification here. Never decode and trust a token without verification. */
|
|
1605
|
+
export async function verifyExternalToken(_token: string): Promise<ExternalAuthPrincipal> {
|
|
1606
|
+
throw new Error('External token verifier is not configured');
|
|
1607
|
+
}
|
|
1608
|
+
`;
|
|
1609
|
+
}
|
|
1610
|
+
function generateExternalTokenHelperTS() {
|
|
1611
|
+
return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1612
|
+
import { verifyExternalToken, ExternalTokenVerificationError, type ExternalAuthPrincipal } from './external-token-verifier';
|
|
1613
|
+
export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
|
|
1614
|
+
export async function requireAuth(request: HttpRequest): Promise<ExternalAuthPrincipal> {
|
|
1615
|
+
const header = request.headers.get('authorization') || '';
|
|
1616
|
+
if (!header.startsWith('Bearer ') || !header.slice(7)) throw new AuthError(401, 'Missing or invalid Authorization header');
|
|
1617
|
+
try { const principal = await verifyExternalToken(header.slice(7));
|
|
1618
|
+
if (!principal?.userId || !Array.isArray(principal.roles)) throw new AuthError(401, 'Invalid external principal');
|
|
1619
|
+
return principal;
|
|
1620
|
+
} catch (error) { if (error instanceof AuthError) throw error; if (error instanceof ExternalTokenVerificationError) throw new AuthError(error.status, error.message); throw new AuthError(500, 'External token verifier is not configured'); }
|
|
1621
|
+
}
|
|
1622
|
+
export function requireRoles(user: ExternalAuthPrincipal, roles: string[]) { if (!roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions'); }
|
|
1623
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null; }
|
|
1624
|
+
`;
|
|
1625
|
+
}
|
|
1626
|
+
function generateExternalAuthMeTS() {
|
|
1627
|
+
return `import { app, HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1628
|
+
import { requireAuth, handleAuthError } from './auth/jwt-helper';
|
|
1629
|
+
app.http('external-auth-me', { methods: ['GET'], route: 'auth/me', authLevel: 'function', handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
|
|
1630
|
+
try { const p = await requireAuth(request); return { status: 200, jsonBody: { id: p.userId, loginId: p.userDetails, name: p.userDetails, email: '', roles: p.roles } }; }
|
|
1631
|
+
catch (error) { return handleAuthError(error) || { status: 500, jsonBody: { error: 'Authentication failed' } }; }
|
|
1632
|
+
} });
|
|
1633
|
+
`;
|
|
1634
|
+
}
|
|
1635
|
+
function generateExternalTokenVerifierCSharp() { return `namespace Functions.Auth;\npublic record ExternalAuthPrincipal(string UserId, string UserDetails, string[] Roles);\npublic static class ExternalTokenVerifier { public static Task<ExternalAuthPrincipal> VerifyExternalToken(string token) => throw new InvalidOperationException("External token verifier is not configured"); }\n`; }
|
|
1636
|
+
function generateExternalTokenHelperCSharp() { return `using Microsoft.Azure.Functions.Worker.Http;\nnamespace Functions.Auth;\npublic static class JwtHelper { public static async Task<(ExternalAuthPrincipal? Principal, HttpResponseData? ErrorResponse)> Authorize(HttpRequestData request, params string[] roles) { if (!request.Headers.TryGetValues("Authorization", out var values) || !values.FirstOrDefault()!.StartsWith("Bearer ")) { var r=request.CreateResponse(System.Net.HttpStatusCode.Unauthorized); await r.WriteAsJsonAsync(new { error="Authentication required" }); return (null,r); } try { var p=await ExternalTokenVerifier.VerifyExternalToken(values.First()[7..]); if (roles.Length>0 && !roles.Any(p.Roles.Contains)) { var r=request.CreateResponse(System.Net.HttpStatusCode.Forbidden); await r.WriteAsJsonAsync(new { error="Insufficient permissions" }); return (null,r); } return (p,null); } catch (HttpRequestException) { var r=request.CreateResponse(System.Net.HttpStatusCode.ServiceUnavailable); await r.WriteAsJsonAsync(new { error="Identity provider unavailable" }); return (null,r); } catch { var r=request.CreateResponse(System.Net.HttpStatusCode.Unauthorized); await r.WriteAsJsonAsync(new { error="Authentication failed" }); return (null,r); } } }\n`; }
|
|
1637
|
+
function generateExternalAuthMeCSharp() { return `using Microsoft.Azure.Functions.Worker;\nusing Microsoft.Azure.Functions.Worker.Http;\nnamespace Functions.Auth;\npublic class ExternalAuthMe { [Function("external-auth-me")] public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function,"get",Route="auth/me")] HttpRequestData req) { var (p,e)=await JwtHelper.Authorize(req); if(e!=null)return e; var r=req.CreateResponse(System.Net.HttpStatusCode.OK); await r.WriteAsJsonAsync(new { id=p!.UserId, loginId=p.UserDetails, name=p.UserDetails, email="", roles=p.Roles }); return r; } }\n`; }
|
|
1638
|
+
function generateExternalTokenVerifierPython() { return `class ExternalTokenVerificationError(Exception):\n def __init__(self, status: int, message: str): self.status, self.message = status, message\n\ndef verify_external_token(token: str) -> dict:\n raise RuntimeError("External token verifier is not configured")\n`; }
|
|
1639
|
+
function generateExternalTokenHelperPython() { return `import azure.functions as func\nfrom .external_token_verifier import verify_external_token, ExternalTokenVerificationError\nclass AuthError(Exception):\n def __init__(self,status,message): self.status,self.message=status,message\ndef require_auth(req: func.HttpRequest):\n header=req.headers.get("authorization","")\n if not header.startswith("Bearer "): raise AuthError(401,"Authentication required")\n try:\n p=verify_external_token(header[7:])\n if not p.get("userId") or not isinstance(p.get("roles"),list): raise AuthError(401,"Invalid external principal")\n return p\n except ExternalTokenVerificationError as e: raise AuthError(e.status,e.message)\n except AuthError: raise\n except RuntimeError: raise AuthError(500,"External token verifier is not configured")\ndef require_roles(user,roles):\n if not any(r in user.get("roles",[]) for r in roles): raise AuthError(403,"Insufficient permissions")\ndef handle_auth_error(error):\n return func.HttpResponse('{"error":"%s"}' % error.message,status_code=error.status,mimetype="application/json") if isinstance(error,AuthError) else None\n`; }
|
|
1640
|
+
function generateExternalAuthMePython() { return `import json\nimport azure.functions as func\nfrom auth.jwt_helper import require_auth, handle_auth_error\nbp=func.Blueprint()\n@bp.route(route="auth/me",methods=["GET"],auth_level=func.AuthLevel.FUNCTION)\ndef auth_me(req: func.HttpRequest):\n try:\n p=require_auth(req); return func.HttpResponse(json.dumps({"id":p["userId"],"loginId":p.get("userDetails",""),"name":p.get("userDetails",""),"email":"","roles":p["roles"]}),mimetype="application/json")\n except Exception as e:\n return handle_auth_error(e) or func.HttpResponse('{"error":"Authentication failed"}',status_code=500,mimetype="application/json")\n`; }
|
|
1641
|
+
function generateSwaAuthHelperPython() {
|
|
1642
|
+
return `import base64
|
|
1643
|
+
import json
|
|
1644
|
+
import azure.functions as func
|
|
1645
|
+
|
|
1646
|
+
class AuthError(Exception):
|
|
1647
|
+
def __init__(self, status, message): self.status, self.message = status, message
|
|
1648
|
+
|
|
1649
|
+
def require_auth(req):
|
|
1650
|
+
encoded = req.headers.get("x-ms-client-principal")
|
|
1651
|
+
if not encoded: raise AuthError(401, "Authentication required")
|
|
1652
|
+
try:
|
|
1653
|
+
value = json.loads(base64.b64decode(encoded).decode("utf-8"))
|
|
1654
|
+
roles = [r for r in value.get("userRoles", []) if isinstance(r, str)]
|
|
1655
|
+
if not value.get("userId") or "authenticated" not in roles: raise ValueError()
|
|
1656
|
+
return {"id": value["userId"], "loginId": value.get("userDetails", ""), "roles": roles}
|
|
1657
|
+
except Exception: raise AuthError(401, "Invalid client principal")
|
|
1658
|
+
|
|
1659
|
+
def require_roles(user, roles):
|
|
1660
|
+
if roles and not any(role in user["roles"] for role in roles): raise AuthError(403, "Insufficient permissions")
|
|
1661
|
+
|
|
1662
|
+
def handle_auth_error(error):
|
|
1663
|
+
return func.HttpResponse(json.dumps({"error": error.message}), status_code=error.status, mimetype="application/json") if isinstance(error, AuthError) else None
|
|
1664
|
+
`;
|
|
1665
|
+
}
|
|
1666
|
+
function generateSwaAuthHelperCSharp() {
|
|
1667
|
+
return `using System.Text;
|
|
1668
|
+
using System.Text.Json;
|
|
1669
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
1670
|
+
|
|
1671
|
+
namespace Functions.Auth;
|
|
1672
|
+
|
|
1673
|
+
public class JwtPayload
|
|
1674
|
+
{
|
|
1675
|
+
public string Sub { get; set; } = "";
|
|
1676
|
+
public string LoginId { get; set; } = "";
|
|
1677
|
+
public string[] Roles { get; set; } = Array.Empty<string>();
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
public static class JwtHelper
|
|
1681
|
+
{
|
|
1682
|
+
public static async Task<(JwtPayload?, HttpResponseData?)> Authorize(HttpRequestData request, params string[] requiredRoles)
|
|
1683
|
+
{
|
|
1684
|
+
try
|
|
1685
|
+
{
|
|
1686
|
+
var encoded = request.Headers.TryGetValues("x-ms-client-principal", out var values) ? values.FirstOrDefault() : null;
|
|
1687
|
+
if (string.IsNullOrWhiteSpace(encoded)) throw new InvalidOperationException();
|
|
1688
|
+
using var document = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(encoded)));
|
|
1689
|
+
var root = document.RootElement;
|
|
1690
|
+
var userId = root.GetProperty("userId").GetString() ?? "";
|
|
1691
|
+
var details = root.TryGetProperty("userDetails", out var detailValue) ? detailValue.GetString() ?? "" : "";
|
|
1692
|
+
var roles = root.TryGetProperty("userRoles", out var roleValue) && roleValue.ValueKind == JsonValueKind.Array
|
|
1693
|
+
? roleValue.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.String).Select(v => v.GetString()!).ToArray()
|
|
1694
|
+
: Array.Empty<string>();
|
|
1695
|
+
if (string.IsNullOrEmpty(userId) || !roles.Contains("authenticated")) throw new InvalidOperationException();
|
|
1696
|
+
if (requiredRoles.Length > 0 && !requiredRoles.Any(roles.Contains))
|
|
1697
|
+
{
|
|
1698
|
+
var forbidden = request.CreateResponse(System.Net.HttpStatusCode.Forbidden);
|
|
1699
|
+
await forbidden.WriteAsJsonAsync(new { error = "Forbidden" });
|
|
1700
|
+
return (null, forbidden);
|
|
1701
|
+
}
|
|
1702
|
+
return (new JwtPayload { Sub = userId, LoginId = details, Roles = roles }, null);
|
|
1703
|
+
}
|
|
1704
|
+
catch
|
|
1705
|
+
{
|
|
1706
|
+
var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
|
|
1707
|
+
await unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" });
|
|
1708
|
+
return (null, unauthorized);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
`;
|
|
1713
|
+
}
|
|
1714
|
+
// ============================================================
|
|
1715
|
+
// 13. scaffold 用ロールガード挿入ヘルパー
|
|
1716
|
+
// ============================================================
|
|
1717
|
+
/**
|
|
1718
|
+
* TypeScript Functions のハンドラーにロールガードを挿入するためのインポート文を生成
|
|
1719
|
+
*/
|
|
1720
|
+
function generateAuthImportTS(namedPolicy = false) {
|
|
1721
|
+
return `import { requireAuth, requireRoles, handleAuthError } from './auth/${namedPolicy ? "auth-router" : "jwt-helper"}';`;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* TypeScript Functions のハンドラー先頭に挿入する認証チェックコードを生成
|
|
1725
|
+
*/
|
|
1726
|
+
function generateAuthGuardTS(policy, operation, provider) {
|
|
1727
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1728
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1729
|
+
const name = typeof selected === 'string' ? selected : policy.policy;
|
|
1730
|
+
return ` const authUser = await requireAuth(request, ${JSON.stringify(name)});`;
|
|
1731
|
+
}
|
|
1732
|
+
const roles = selected || policy.roles || [];
|
|
1733
|
+
if (roles.length > 0) {
|
|
1734
|
+
return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);
|
|
1735
|
+
requireRoles(authUser, ${JSON.stringify(roles)});`;
|
|
1736
|
+
}
|
|
1737
|
+
return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);`;
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* C# Functions のハンドラーに挿入するロールガードコードを生成
|
|
1741
|
+
*/
|
|
1742
|
+
function generateAuthGuardCSharp(policy, operation) {
|
|
1743
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1744
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1745
|
+
const name = typeof selected === 'string' ? selected : policy.policy;
|
|
1746
|
+
return ` var (principal, errorResponse) = await AuthRouter.Authorize(request, ${JSON.stringify(name)});\n if (errorResponse != null) return errorResponse;`;
|
|
1747
|
+
}
|
|
1748
|
+
const roles = selected || policy.roles || [];
|
|
1749
|
+
if (roles.length > 0) {
|
|
1750
|
+
const rolesStr = roles.map(r => `"${r}"`).join(', ');
|
|
1751
|
+
return ` var (principal, errorResponse) = await JwtHelper.Authorize(request, ${rolesStr});
|
|
1752
|
+
if (errorResponse != null) return errorResponse;`;
|
|
1753
|
+
}
|
|
1754
|
+
return ` var (principal, errorResponse) = await JwtHelper.Authorize(request);
|
|
1755
|
+
if (errorResponse != null) return errorResponse;`;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Python Functions のハンドラーに挿入するロールガードコードを生成
|
|
1759
|
+
*/
|
|
1760
|
+
function generateAuthGuardPython(policy, operation) {
|
|
1761
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1762
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1763
|
+
const name = typeof selected === 'string' ? selected : policy.policy;
|
|
1764
|
+
return ` user = require_auth(req, ${JSON.stringify(name)})`;
|
|
1765
|
+
}
|
|
1766
|
+
const roles = selected || policy.roles || [];
|
|
1767
|
+
if (roles.length > 0) {
|
|
1768
|
+
const rolesStr = roles.map(r => `"${r}"`).join(', ');
|
|
1769
|
+
return ` user = require_auth(req)
|
|
1770
|
+
require_roles(user, [${rolesStr}])`;
|
|
1771
|
+
}
|
|
1772
|
+
return ` user = require_auth(req)`;
|
|
1773
|
+
}
|
|
1774
|
+
//# sourceMappingURL=auth-generator.js.map
|