swallowkit 1.0.0-beta.46 → 1.0.0-beta.48
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/dist/cli/commands/add-auth.d.ts +3 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +350 -17
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +4 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +104 -13
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +56 -30
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +9 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +84 -8
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +7 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +81 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.js +2 -2
- package/dist/core/mock/connector-mock-server.js.map +1 -1
- package/dist/core/project/manifest.js +2 -2
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +17 -0
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts +28 -3
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +493 -23
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.js +23 -21
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts +2 -2
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +20 -18
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +8 -3
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +13 -1
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +3 -0
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +12 -8
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/types/index.d.ts +35 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +1 -0
- package/dist/utils/package-manager.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +20 -20
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +13 -1
- package/src/__tests__/auth.test.ts +107 -0
- package/src/__tests__/config.test.ts +22 -1
- package/src/__tests__/dev.test.ts +27 -0
- package/src/__tests__/init.test.ts +19 -0
- package/src/__tests__/mcp.test.ts +2 -2
- package/src/__tests__/multi-auth.test.ts +133 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/cli/commands/add-auth.ts +351 -18
- package/src/cli/commands/dev.ts +116 -16
- package/src/cli/commands/init.ts +56 -30
- package/src/cli/commands/scaffold.ts +86 -10
- package/src/cli/index.ts +5 -1
- package/src/core/config.ts +69 -2
- package/src/core/mock/connector-mock-server.ts +2 -2
- package/src/core/project/manifest.ts +3 -3
- package/src/core/project/validation.ts +16 -0
- package/src/core/scaffold/auth-generator.ts +500 -24
- package/src/core/scaffold/connector-functions-generator.ts +26 -21
- package/src/core/scaffold/functions-generator.ts +21 -19
- package/src/core/scaffold/model-parser.ts +9 -3
- package/src/core/scaffold/nextjs-generator.ts +13 -1
- package/src/core/scaffold/ui-generator.ts +15 -8
- package/src/types/index.ts +36 -5
- package/src/utils/package-manager.ts +1 -0
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* add-auth コマンドおよび scaffold ロールガード挿入で使用するテンプレート群
|
|
5
5
|
*/
|
|
6
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;
|
|
7
12
|
exports.generateAuthModels = generateAuthModels;
|
|
8
13
|
exports.generateAuthFunctionsTS = generateAuthFunctionsTS;
|
|
9
14
|
exports.generateJwtHelperTS = generateJwtHelperTS;
|
|
@@ -18,10 +23,223 @@ exports.generateProxy = generateProxy;
|
|
|
18
23
|
exports.generateLoginPage = generateLoginPage;
|
|
19
24
|
exports.generateAuthContext = generateAuthContext;
|
|
20
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;
|
|
21
44
|
exports.generateAuthImportTS = generateAuthImportTS;
|
|
22
45
|
exports.generateAuthGuardTS = generateAuthGuardTS;
|
|
23
46
|
exports.generateAuthGuardCSharp = generateAuthGuardCSharp;
|
|
24
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
|
+
${imports}
|
|
62
|
+
export type AuthPrincipal = { subject: string; scheme: string; issuer: string; roles: string[]; claims: Record<string, unknown>; /** @deprecated */ userId?: string; /** @deprecated */ userDetails?: string };
|
|
63
|
+
export class AuthError extends Error { constructor(public status: 401 | 403 | 503, message: string) { super(message); } }
|
|
64
|
+
const schemes = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n, s]) => [n, { provider: s.provider, allowedProviders: s.swa?.allowedProviders ?? [] }])))} as const;
|
|
65
|
+
const policies = ${JSON.stringify(config.authorization.policies)} as Record<string, { schemes: string[]; roles?: string[] }>;
|
|
66
|
+
const externalVerifiers: Record<string, (token: string) => Promise<any>> = { ${verifiers} };
|
|
67
|
+
const customVerifiers: Record<string, (request: HttpRequest) => any> = { ${custom.map(([name]) => `${JSON.stringify(name)}: verify_${safeIdentifier(name)}`).join(", ")} };
|
|
68
|
+
function swa(request: HttpRequest, scheme: string): AuthPrincipal {
|
|
69
|
+
const encoded = request.headers.get('x-ms-client-principal');
|
|
70
|
+
if (!encoded) throw new AuthError(401, 'Authentication required');
|
|
71
|
+
try { const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) as Record<string, unknown>;
|
|
72
|
+
const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r): r is string => typeof r === 'string') : [];
|
|
73
|
+
if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
|
|
74
|
+
const allowed = (schemes as any)[scheme]?.allowedProviders as string[] | undefined;
|
|
75
|
+
if (allowed?.length && (typeof value.identityProvider !== 'string' || !allowed.includes(value.identityProvider))) throw new Error();
|
|
76
|
+
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 };
|
|
77
|
+
} catch (e) { if (e instanceof AuthError) throw e; throw new AuthError(401, 'Invalid SWA client principal'); }
|
|
78
|
+
}
|
|
79
|
+
export async function requireAuth(request: HttpRequest, policyName: string): Promise<AuthPrincipal> {
|
|
80
|
+
const policy = policies[policyName]; if (!policy) throw new AuthError(401, 'Undefined authorization policy');
|
|
81
|
+
const bearer = request.headers.get('authorization'); const hasSwa = Boolean(request.headers.get('x-ms-client-principal'));
|
|
82
|
+
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; });
|
|
83
|
+
if (candidates.length !== 1) throw new AuthError(401, candidates.length ? 'Ambiguous credentials' : 'Authentication required');
|
|
84
|
+
const scheme = candidates[0]; const provider = (schemes as any)[scheme]?.provider; let principal: AuthPrincipal;
|
|
85
|
+
if (provider === 'swa') principal = swa(request, scheme);
|
|
86
|
+
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'); } }
|
|
87
|
+
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'); } }
|
|
88
|
+
else throw new AuthError(401, 'Authentication adapter is not configured');
|
|
89
|
+
requireRoles(principal, policy.roles || []); return principal;
|
|
90
|
+
}
|
|
91
|
+
export function requireRoles(principal: AuthPrincipal, roles: string[]): void { if (roles.length && !roles.some(r => principal.roles.includes(r))) throw new AuthError(403, 'Insufficient permissions'); }
|
|
92
|
+
export function identityKey(principal: AuthPrincipal): string { return principal.scheme + ':' + principal.subject; }
|
|
93
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status:error.status, jsonBody:{error:error.message} } : null; }
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
function safeIdentifier(name) {
|
|
97
|
+
return name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
98
|
+
}
|
|
99
|
+
function schemeSlug(name) {
|
|
100
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
function generateNamedExternalTokenVerifierCSharp(scheme) {
|
|
103
|
+
const id = safeIdentifier(scheme);
|
|
104
|
+
return `namespace Functions.Auth.Schemes;
|
|
105
|
+
public record ExternalPrincipal_${id}(string Subject, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims);
|
|
106
|
+
public sealed class ExternalTokenVerificationException_${id} : Exception
|
|
107
|
+
{
|
|
108
|
+
public bool IsIdentityProviderUnavailable { get; }
|
|
109
|
+
public ExternalTokenVerificationException_${id}(string message, bool unavailable = false) : base(message) => IsIdentityProviderUnavailable = unavailable;
|
|
110
|
+
}
|
|
111
|
+
public static class ExternalTokenVerifier_${id}
|
|
112
|
+
{
|
|
113
|
+
// Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
|
|
114
|
+
public static Task<ExternalPrincipal_${id}> VerifyAsync(string token) => throw new InvalidOperationException("External token verifier for ${scheme} is not configured");
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
function generateNamedExternalTokenVerifierPython(scheme) {
|
|
119
|
+
return `class ExternalTokenVerificationError(Exception):
|
|
120
|
+
def __init__(self, message: str, identity_provider_unavailable: bool = False):
|
|
121
|
+
super().__init__(message)
|
|
122
|
+
self.identity_provider_unavailable = identity_provider_unavailable
|
|
123
|
+
|
|
124
|
+
def verify_external_token(token: str) -> dict:
|
|
125
|
+
# Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
|
|
126
|
+
raise RuntimeError("External token verifier for ${scheme} is not configured")
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
function generateNamedAuthRouterCSharp(config) {
|
|
130
|
+
const externalCases = Object.entries(config.schemes).filter(([, s]) => s.provider === "external-token").map(([name]) => {
|
|
131
|
+
const id = safeIdentifier(name);
|
|
132
|
+
return ` case "${name}":
|
|
133
|
+
try
|
|
134
|
+
{
|
|
135
|
+
var external = await ExternalTokenVerifier_${id}.VerifyAsync(bearer!);
|
|
136
|
+
principal = new AuthPrincipal(external.Subject, "${name}", external.Issuer, external.Roles, external.Claims);
|
|
137
|
+
}
|
|
138
|
+
catch (ExternalTokenVerificationException_${id} ex) when (ex.IsIdentityProviderUnavailable) { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "Identity provider unavailable")); }
|
|
139
|
+
catch (ExternalTokenVerificationException_${id}) { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid token")); }
|
|
140
|
+
catch { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "External token verifier unavailable")); }
|
|
141
|
+
break;`;
|
|
142
|
+
}).join("\n");
|
|
143
|
+
return `using System.Net;
|
|
144
|
+
using System.Text;
|
|
145
|
+
using System.Text.Json;
|
|
146
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
147
|
+
using Functions.Auth.Schemes;
|
|
148
|
+
namespace Functions.Auth;
|
|
149
|
+
public record AuthPrincipal(string Subject, string Scheme, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims)
|
|
150
|
+
{
|
|
151
|
+
[Obsolete("Use Subject")] public string UserId => Subject;
|
|
152
|
+
public string IdentityKey => $"{Scheme}:{Subject}";
|
|
153
|
+
}
|
|
154
|
+
public static class AuthRouter
|
|
155
|
+
{
|
|
156
|
+
private static readonly Dictionary<string, (string[] Schemes, string[] Roles)> Policies = new()
|
|
157
|
+
{
|
|
158
|
+
${Object.entries(config.authorization.policies).map(([n, p]) => ` ["${n}"] = (new[] { ${p.schemes.map(s => `"${s}"`).join(", ")} }, new[] { ${(p.roles ?? []).map(r => `"${r}"`).join(", ")} }),`).join("\n")}
|
|
159
|
+
};
|
|
160
|
+
private static readonly Dictionary<string,string> Providers = new() { ${Object.entries(config.schemes).map(([n, s]) => `["${n}"]="${s.provider}"`).join(", ")} };
|
|
161
|
+
private static readonly Dictionary<string,string[]> AllowedProviders = new() { ${Object.entries(config.schemes).map(([n, s]) => `["${n}"]=new[] { ${(s.swa?.allowedProviders ?? []).map(p => `"${p}"`).join(", ")} }`).join(", ")} };
|
|
162
|
+
public static async Task<(AuthPrincipal? Principal, HttpResponseData? ErrorResponse)> Authorize(HttpRequestData request, string policyName)
|
|
163
|
+
{
|
|
164
|
+
if (!Policies.TryGetValue(policyName, out var policy)) return (null, await Error(request, HttpStatusCode.Unauthorized, "Undefined authorization policy"));
|
|
165
|
+
var bearerHeader = request.Headers.TryGetValues("Authorization", out var authValues) ? authValues.FirstOrDefault() : null;
|
|
166
|
+
var bearer = bearerHeader?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) == true ? bearerHeader[7..] : null;
|
|
167
|
+
var swa = request.Headers.TryGetValues("x-ms-client-principal", out var swaValues) ? swaValues.FirstOrDefault() : null;
|
|
168
|
+
var candidates = policy.Schemes.Where(s => Providers[s] == "swa" ? swa != null : Providers[s] is "external-token" or "custom-jwt" ? bearer != null : false).ToArray();
|
|
169
|
+
if (candidates.Length != 1) return (null, await Error(request, HttpStatusCode.Unauthorized, candidates.Length == 0 ? "Authentication required" : "Ambiguous credentials"));
|
|
170
|
+
var scheme = candidates[0]; AuthPrincipal principal;
|
|
171
|
+
switch (scheme)
|
|
172
|
+
{
|
|
173
|
+
${externalCases}
|
|
174
|
+
default:
|
|
175
|
+
if (Providers[scheme] != "swa") return (null, await Error(request, HttpStatusCode.Unauthorized, "Authentication adapter is not configured"));
|
|
176
|
+
try
|
|
177
|
+
{
|
|
178
|
+
using var doc = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(swa!)));
|
|
179
|
+
var root = doc.RootElement; var subject = root.GetProperty("userId").GetString();
|
|
180
|
+
var roles = root.TryGetProperty("userRoles", out var rv) ? rv.EnumerateArray().Where(v=>v.ValueKind==JsonValueKind.String).Select(v=>v.GetString()!).ToArray() : Array.Empty<string>();
|
|
181
|
+
if (string.IsNullOrEmpty(subject) || !roles.Contains("authenticated")) throw new InvalidOperationException();
|
|
182
|
+
var identityProvider = root.TryGetProperty("identityProvider", out var iv) ? iv.GetString() : null;
|
|
183
|
+
if (AllowedProviders[scheme].Length > 0 && (identityProvider == null || !AllowedProviders[scheme].Contains(identityProvider))) throw new InvalidOperationException();
|
|
184
|
+
var issuer = identityProvider == null ? "swa" : "swa:" + identityProvider;
|
|
185
|
+
principal = new AuthPrincipal(subject!, scheme, issuer, roles, new Dictionary<string,object?> { ["clientPrincipal"] = root.Clone() });
|
|
186
|
+
}
|
|
187
|
+
catch { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid SWA client principal")); }
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
if (policy.Roles.Length > 0 && !policy.Roles.Any(principal.Roles.Contains)) return (null, await Error(request, HttpStatusCode.Forbidden, "Insufficient permissions"));
|
|
191
|
+
return (principal, null);
|
|
192
|
+
}
|
|
193
|
+
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; }
|
|
194
|
+
}
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
function generateNamedAuthRouterPython(config) {
|
|
198
|
+
const external = Object.entries(config.schemes).filter(([, s]) => s.provider === "external-token");
|
|
199
|
+
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");
|
|
200
|
+
const cases = external.map(([n], index) => ` ${index === 0 ? "if" : "elif"} scheme == ${JSON.stringify(n)}:
|
|
201
|
+
try:
|
|
202
|
+
value = verify_${safeIdentifier(n)}(bearer)
|
|
203
|
+
except Error_${safeIdentifier(n)} as exc:
|
|
204
|
+
raise AuthError(503 if exc.identity_provider_unavailable else 401, "Identity provider unavailable" if exc.identity_provider_unavailable else "Invalid token")
|
|
205
|
+
except Exception:
|
|
206
|
+
raise AuthError(503, "External token verifier unavailable")
|
|
207
|
+
if not value.get("subject") or not isinstance(value.get("roles"), list): raise AuthError(401, "Invalid external principal")
|
|
208
|
+
principal = {"subject": value["subject"], "scheme": scheme, "issuer": value.get("issuer", scheme), "roles": value["roles"], "claims": value.get("claims", {}), "userId": value["subject"]}`).join("\n");
|
|
209
|
+
return `import base64
|
|
210
|
+
import json
|
|
211
|
+
import azure.functions as func
|
|
212
|
+
${imports}
|
|
213
|
+
POLICIES = ${JSON.stringify(config.authorization.policies)}
|
|
214
|
+
PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n, s]) => [n, s.provider])))}
|
|
215
|
+
ALLOWED_PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n, s]) => [n, s.swa?.allowedProviders ?? []])))}
|
|
216
|
+
class AuthError(Exception):
|
|
217
|
+
def __init__(self, status, message): self.status, self.message = status, message
|
|
218
|
+
def require_auth(req: func.HttpRequest, policy_name: str):
|
|
219
|
+
policy = POLICIES.get(policy_name)
|
|
220
|
+
if not policy: raise AuthError(401, "Undefined authorization policy")
|
|
221
|
+
header = req.headers.get("authorization", "")
|
|
222
|
+
bearer = header[7:] if header.lower().startswith("bearer ") else None
|
|
223
|
+
swa = req.headers.get("x-ms-client-principal")
|
|
224
|
+
candidates = [s for s in policy["schemes"] if (PROVIDERS[s] == "swa" and swa) or (PROVIDERS[s] in ("external-token", "custom-jwt") and bearer)]
|
|
225
|
+
if len(candidates) != 1: raise AuthError(401, "Authentication required" if not candidates else "Ambiguous credentials")
|
|
226
|
+
scheme = candidates[0]
|
|
227
|
+
${cases}
|
|
228
|
+
${external.length ? "elif" : "if"} PROVIDERS[scheme] == "swa":
|
|
229
|
+
try:
|
|
230
|
+
value=json.loads(base64.b64decode(swa).decode("utf-8")); roles=[r for r in value.get("userRoles",[]) if isinstance(r,str)]
|
|
231
|
+
if not value.get("userId") or "authenticated" not in roles: raise ValueError()
|
|
232
|
+
if ALLOWED_PROVIDERS[scheme] and value.get("identityProvider") not in ALLOWED_PROVIDERS[scheme]: raise ValueError()
|
|
233
|
+
principal={"subject":value["userId"],"scheme":scheme,"issuer":"swa:"+value.get("identityProvider", "unknown"),"roles":roles,"claims":value,"userId":value["userId"]}
|
|
234
|
+
except Exception: raise AuthError(401,"Invalid SWA client principal")
|
|
235
|
+
else: raise AuthError(401,"Authentication adapter is not configured")
|
|
236
|
+
require_roles(principal, policy.get("roles", [])); return principal
|
|
237
|
+
def require_roles(principal, roles):
|
|
238
|
+
if roles and not any(role in principal.get("roles",[]) for role in roles): raise AuthError(403,"Insufficient permissions")
|
|
239
|
+
def identity_key(principal): return principal["scheme"] + ":" + principal["subject"]
|
|
240
|
+
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
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
25
243
|
// ============================================================
|
|
26
244
|
// 1. shared/models/auth.ts(Zod スキーマ)
|
|
27
245
|
// ============================================================
|
|
@@ -113,7 +331,7 @@ ${getConnection}
|
|
|
113
331
|
app.http('auth-login', {
|
|
114
332
|
methods: ['POST'],
|
|
115
333
|
route: 'auth/login',
|
|
116
|
-
authLevel: '
|
|
334
|
+
authLevel: 'function',
|
|
117
335
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
118
336
|
try {
|
|
119
337
|
const body = LoginRequest.parse(await request.json());
|
|
@@ -171,7 +389,7 @@ app.http('auth-login', {
|
|
|
171
389
|
app.http('auth-me', {
|
|
172
390
|
methods: ['GET'],
|
|
173
391
|
route: 'auth/me',
|
|
174
|
-
authLevel: '
|
|
392
|
+
authLevel: 'function',
|
|
175
393
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
176
394
|
try {
|
|
177
395
|
const user = requireAuth(request);
|
|
@@ -189,7 +407,7 @@ app.http('auth-me', {
|
|
|
189
407
|
app.http('auth-logout', {
|
|
190
408
|
methods: ['POST'],
|
|
191
409
|
route: 'auth/logout',
|
|
192
|
-
authLevel: '
|
|
410
|
+
authLevel: 'function',
|
|
193
411
|
handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
|
|
194
412
|
return { status: 200, jsonBody: { message: 'Logged out' } };
|
|
195
413
|
},
|
|
@@ -304,7 +522,7 @@ namespace Functions.Auth
|
|
|
304
522
|
{
|
|
305
523
|
[Function("auth-login")]
|
|
306
524
|
public async Task<HttpResponseData> Login(
|
|
307
|
-
[HttpTrigger(AuthorizationLevel.
|
|
525
|
+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/login")] HttpRequestData request)
|
|
308
526
|
{
|
|
309
527
|
var body = await JsonSerializer.DeserializeAsync<LoginRequest>(request.Body);
|
|
310
528
|
if (body == null || string.IsNullOrEmpty(body.LoginId) || string.IsNullOrEmpty(body.Password))
|
|
@@ -363,7 +581,7 @@ namespace Functions.Auth
|
|
|
363
581
|
|
|
364
582
|
[Function("auth-me")]
|
|
365
583
|
public async Task<HttpResponseData> Me(
|
|
366
|
-
[HttpTrigger(AuthorizationLevel.
|
|
584
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "auth/me")] HttpRequestData request)
|
|
367
585
|
{
|
|
368
586
|
var (principal, errorResponse) = await JwtHelper.Authorize(request);
|
|
369
587
|
if (errorResponse != null) return errorResponse;
|
|
@@ -382,7 +600,7 @@ namespace Functions.Auth
|
|
|
382
600
|
|
|
383
601
|
[Function("auth-logout")]
|
|
384
602
|
public async Task<HttpResponseData> Logout(
|
|
385
|
-
[HttpTrigger(AuthorizationLevel.
|
|
603
|
+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/logout")] HttpRequestData request)
|
|
386
604
|
{
|
|
387
605
|
var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
|
|
388
606
|
await response.WriteAsJsonAsync(new { message = "Logged out" });
|
|
@@ -561,7 +779,7 @@ auth_bp = func.Blueprint()
|
|
|
561
779
|
${getConnection}
|
|
562
780
|
|
|
563
781
|
|
|
564
|
-
@auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.
|
|
782
|
+
@auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
|
|
565
783
|
def auth_login(req: func.HttpRequest) -> func.HttpResponse:
|
|
566
784
|
try:
|
|
567
785
|
body = req.get_json()
|
|
@@ -633,7 +851,7 @@ def auth_login(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
633
851
|
)
|
|
634
852
|
|
|
635
853
|
|
|
636
|
-
@auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.
|
|
854
|
+
@auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.FUNCTION)
|
|
637
855
|
def auth_me(req: func.HttpRequest) -> func.HttpResponse:
|
|
638
856
|
from auth.jwt_helper import require_auth, handle_auth_error
|
|
639
857
|
|
|
@@ -652,7 +870,7 @@ def auth_me(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
652
870
|
)
|
|
653
871
|
|
|
654
872
|
|
|
655
|
-
@auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.
|
|
873
|
+
@auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
|
|
656
874
|
def auth_logout(req: func.HttpRequest) -> func.HttpResponse:
|
|
657
875
|
return func.HttpResponse(
|
|
658
876
|
json.dumps({"message": "Logged out"}),
|
|
@@ -1089,6 +1307,17 @@ function getFunctionsBaseUrl(): string {
|
|
|
1089
1307
|
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
1090
1308
|
}
|
|
1091
1309
|
|
|
1310
|
+
function isLocalFunctionsUrl(url: string): boolean {
|
|
1311
|
+
return url.startsWith('http://localhost:') || url.startsWith('http://127.0.0.1:');
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
1315
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
1316
|
+
if (key) return { 'x-functions-key': key };
|
|
1317
|
+
if (isLocalFunctionsUrl(functionsBaseUrl)) return {};
|
|
1318
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1092
1321
|
interface CallFunctionConfig<TInput = any, TOutput = any> {
|
|
1093
1322
|
/** HTTP メソッド */
|
|
1094
1323
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
@@ -1127,11 +1356,13 @@ export async function callFunction<TInput = any, TOutput = any>(
|
|
|
1127
1356
|
// Azure Functions を呼び出し
|
|
1128
1357
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
1129
1358
|
const url = functionsBaseUrl + path;
|
|
1359
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
1130
1360
|
console.log(\`[BFF] \${method} \${url}\`);
|
|
1131
1361
|
|
|
1132
1362
|
// Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)
|
|
1133
1363
|
const fetchHeaders: Record<string, string> = {
|
|
1134
1364
|
'Content-Type': 'application/json',
|
|
1365
|
+
...keyHeaders,
|
|
1135
1366
|
};
|
|
1136
1367
|
try {
|
|
1137
1368
|
const reqHeaders = await headers();
|
|
@@ -1190,35 +1421,271 @@ export async function callFunction<TInput = any, TOutput = any>(
|
|
|
1190
1421
|
}
|
|
1191
1422
|
`;
|
|
1192
1423
|
}
|
|
1424
|
+
/** SWA built-in auth variant: forward only a platform-derived client principal. */
|
|
1425
|
+
function generateBFFCallFunctionWithSwaAuth() {
|
|
1426
|
+
return generateBFFCallFunctionWithAuth()
|
|
1427
|
+
.replace(` // Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)`, ` // SWA client principal is accepted only from the platform request context.`)
|
|
1428
|
+
.replace(` const authorization = reqHeaders.get('authorization');
|
|
1429
|
+
if (authorization) {
|
|
1430
|
+
fetchHeaders['Authorization'] = authorization;
|
|
1431
|
+
}`, ` // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.
|
|
1432
|
+
let principal: string | null = null;
|
|
1433
|
+
{
|
|
1434
|
+
const cookie = reqHeaders.get('cookie');
|
|
1435
|
+
const host = reqHeaders.get('host');
|
|
1436
|
+
if (cookie && host) {
|
|
1437
|
+
const proto = reqHeaders.get('x-forwarded-proto') || 'https';
|
|
1438
|
+
const meResponse = await fetch(\`\${proto}://\${host}/.auth/me\`, {
|
|
1439
|
+
headers: { cookie },
|
|
1440
|
+
cache: 'no-store',
|
|
1441
|
+
});
|
|
1442
|
+
if (meResponse.ok) {
|
|
1443
|
+
const payload = await meResponse.json() as { clientPrincipal?: unknown };
|
|
1444
|
+
if (payload.clientPrincipal) {
|
|
1445
|
+
principal = Buffer.from(JSON.stringify(payload.clientPrincipal), 'utf8').toString('base64');
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (principal) {
|
|
1451
|
+
const parsed = JSON.parse(Buffer.from(principal, 'base64').toString('utf8'));
|
|
1452
|
+
const normalized = {
|
|
1453
|
+
identityProvider: String(parsed.identityProvider || ''),
|
|
1454
|
+
userId: String(parsed.userId || ''),
|
|
1455
|
+
userDetails: String(parsed.userDetails || ''),
|
|
1456
|
+
userRoles: Array.isArray(parsed.userRoles) ? parsed.userRoles.filter((role: unknown) => typeof role === 'string') : [],
|
|
1457
|
+
};
|
|
1458
|
+
if (normalized.userId && normalized.userRoles.includes('authenticated')) {
|
|
1459
|
+
fetchHeaders['x-ms-client-principal'] = Buffer.from(JSON.stringify(normalized), 'utf8').toString('base64');
|
|
1460
|
+
}
|
|
1461
|
+
}`);
|
|
1462
|
+
}
|
|
1463
|
+
/** Combined transport for named schemes. Functions still performs all verification. */
|
|
1464
|
+
function generateBFFCallFunctionWithMultipleAuth() {
|
|
1465
|
+
return generateBFFCallFunctionWithSwaAuth().replace(` // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.`, ` const authorization = reqHeaders.get('authorization');
|
|
1466
|
+
if (authorization?.startsWith('Bearer ')) fetchHeaders['Authorization'] = authorization;
|
|
1467
|
+
// Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.`);
|
|
1468
|
+
}
|
|
1469
|
+
function generateSwaAuthHelperTS() {
|
|
1470
|
+
return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1471
|
+
|
|
1472
|
+
export interface AuthUser { id: string; loginId: string; roles: string[]; }
|
|
1473
|
+
export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
|
|
1474
|
+
|
|
1475
|
+
export function requireAuth(request: HttpRequest): AuthUser {
|
|
1476
|
+
const encoded = request.headers.get('x-ms-client-principal');
|
|
1477
|
+
if (!encoded) throw new AuthError(401, 'Authentication required');
|
|
1478
|
+
try {
|
|
1479
|
+
const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
|
|
1480
|
+
const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r: unknown) => typeof r === 'string') : [];
|
|
1481
|
+
if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
|
|
1482
|
+
return { id: value.userId, loginId: typeof value.userDetails === 'string' ? value.userDetails : '', roles };
|
|
1483
|
+
} catch { throw new AuthError(401, 'Invalid client principal'); }
|
|
1484
|
+
}
|
|
1485
|
+
export function requireRoles(user: AuthUser, roles: string[]): void {
|
|
1486
|
+
if (roles.length && !roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions');
|
|
1487
|
+
}
|
|
1488
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null {
|
|
1489
|
+
return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null;
|
|
1490
|
+
}
|
|
1491
|
+
`;
|
|
1492
|
+
}
|
|
1493
|
+
// ============================================================
|
|
1494
|
+
// External token authentication
|
|
1495
|
+
// ============================================================
|
|
1496
|
+
function generateExternalTokenAdapter() {
|
|
1497
|
+
return `/** Replace these functions with your external IdP SDK integration. */
|
|
1498
|
+
export async function getToken(): Promise<string | null> { return null; }
|
|
1499
|
+
export async function login(): Promise<void> { throw new Error('External token login adapter is not configured'); }
|
|
1500
|
+
export async function logout(): Promise<void> { throw new Error('External token logout adapter is not configured'); }
|
|
1501
|
+
`;
|
|
1502
|
+
}
|
|
1503
|
+
function generateAuthenticatedFetch() {
|
|
1504
|
+
return `import { getToken } from './external-token-adapter';
|
|
1505
|
+
|
|
1506
|
+
export async function authenticatedFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
|
|
1507
|
+
const token = await getToken();
|
|
1508
|
+
if (!token) return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'content-type': 'application/json' } });
|
|
1509
|
+
const headers = new Headers(init.headers);
|
|
1510
|
+
headers.set('Authorization', \`Bearer \${token}\`);
|
|
1511
|
+
return fetch(input, { ...init, headers });
|
|
1512
|
+
}
|
|
1513
|
+
`;
|
|
1514
|
+
}
|
|
1515
|
+
function generateExternalTokenBFFMeRoute() {
|
|
1516
|
+
return `import { callFunction } from '@/lib/api/call-function';
|
|
1517
|
+
export async function GET() { return callFunction({ method: 'GET', path: '/api/auth/me' }); }
|
|
1518
|
+
`;
|
|
1519
|
+
}
|
|
1520
|
+
function generateExternalTokenAuthContext() {
|
|
1521
|
+
return `'use client';
|
|
1522
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
|
1523
|
+
import { authenticatedFetch } from './authenticated-fetch';
|
|
1524
|
+
import * as adapter from './external-token-adapter';
|
|
1525
|
+
export interface AuthUser { id: string; loginId: string; name: string; email: string; roles: string[]; }
|
|
1526
|
+
interface Value { user: AuthUser | null; loading: boolean; login: () => Promise<void>; logout: () => Promise<void>; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; }
|
|
1527
|
+
const Context = createContext<Value | null>(null);
|
|
1528
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
1529
|
+
const [user, setUser] = useState<AuthUser | null>(null); const [loading, setLoading] = useState(true);
|
|
1530
|
+
useEffect(() => { authenticatedFetch('/api/auth/me').then(async r => {
|
|
1531
|
+
if (!r.ok || !(r.headers.get('content-type') || '').includes('application/json')) return null;
|
|
1532
|
+
return r.json() as Promise<AuthUser>;
|
|
1533
|
+
}).then(setUser).catch(() => setUser(null)).finally(() => setLoading(false)); }, []);
|
|
1534
|
+
const hasRole = (role: string) => user?.roles.includes(role) ?? false;
|
|
1535
|
+
const logout = async () => { await adapter.logout(); setUser(null); };
|
|
1536
|
+
return <Context.Provider value={{ user, loading, login: adapter.login, logout, hasRole, hasAnyRole: roles => roles.some(hasRole) }}>{children}</Context.Provider>;
|
|
1537
|
+
}
|
|
1538
|
+
export function useAuth() { const value = useContext(Context); if (!value) throw new Error('useAuth must be used within an AuthProvider'); return value; }
|
|
1539
|
+
`;
|
|
1540
|
+
}
|
|
1541
|
+
function generateExternalTokenVerifierTS() {
|
|
1542
|
+
return `export interface ExternalAuthPrincipal { userId: string; userDetails: string; roles: string[]; }
|
|
1543
|
+
export class ExternalTokenVerificationError extends Error { constructor(public status: 401 | 503, message: string) { super(message); } }
|
|
1544
|
+
/** Implement IdP verification here. Never decode and trust a token without verification. */
|
|
1545
|
+
export async function verifyExternalToken(_token: string): Promise<ExternalAuthPrincipal> {
|
|
1546
|
+
throw new Error('External token verifier is not configured');
|
|
1547
|
+
}
|
|
1548
|
+
`;
|
|
1549
|
+
}
|
|
1550
|
+
function generateExternalTokenHelperTS() {
|
|
1551
|
+
return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1552
|
+
import { verifyExternalToken, ExternalTokenVerificationError, type ExternalAuthPrincipal } from './external-token-verifier';
|
|
1553
|
+
export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
|
|
1554
|
+
export async function requireAuth(request: HttpRequest): Promise<ExternalAuthPrincipal> {
|
|
1555
|
+
const header = request.headers.get('authorization') || '';
|
|
1556
|
+
if (!header.startsWith('Bearer ') || !header.slice(7)) throw new AuthError(401, 'Missing or invalid Authorization header');
|
|
1557
|
+
try { const principal = await verifyExternalToken(header.slice(7));
|
|
1558
|
+
if (!principal?.userId || !Array.isArray(principal.roles)) throw new AuthError(401, 'Invalid external principal');
|
|
1559
|
+
return principal;
|
|
1560
|
+
} 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'); }
|
|
1561
|
+
}
|
|
1562
|
+
export function requireRoles(user: ExternalAuthPrincipal, roles: string[]) { if (!roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions'); }
|
|
1563
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null; }
|
|
1564
|
+
`;
|
|
1565
|
+
}
|
|
1566
|
+
function generateExternalAuthMeTS() {
|
|
1567
|
+
return `import { app, HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1568
|
+
import { requireAuth, handleAuthError } from './auth/jwt-helper';
|
|
1569
|
+
app.http('external-auth-me', { methods: ['GET'], route: 'auth/me', authLevel: 'function', handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
|
|
1570
|
+
try { const p = await requireAuth(request); return { status: 200, jsonBody: { id: p.userId, loginId: p.userDetails, name: p.userDetails, email: '', roles: p.roles } }; }
|
|
1571
|
+
catch (error) { return handleAuthError(error) || { status: 500, jsonBody: { error: 'Authentication failed' } }; }
|
|
1572
|
+
} });
|
|
1573
|
+
`;
|
|
1574
|
+
}
|
|
1575
|
+
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`; }
|
|
1576
|
+
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`; }
|
|
1577
|
+
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`; }
|
|
1578
|
+
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`; }
|
|
1579
|
+
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`; }
|
|
1580
|
+
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`; }
|
|
1581
|
+
function generateSwaAuthHelperPython() {
|
|
1582
|
+
return `import base64
|
|
1583
|
+
import json
|
|
1584
|
+
import azure.functions as func
|
|
1585
|
+
|
|
1586
|
+
class AuthError(Exception):
|
|
1587
|
+
def __init__(self, status, message): self.status, self.message = status, message
|
|
1588
|
+
|
|
1589
|
+
def require_auth(req):
|
|
1590
|
+
encoded = req.headers.get("x-ms-client-principal")
|
|
1591
|
+
if not encoded: raise AuthError(401, "Authentication required")
|
|
1592
|
+
try:
|
|
1593
|
+
value = json.loads(base64.b64decode(encoded).decode("utf-8"))
|
|
1594
|
+
roles = [r for r in value.get("userRoles", []) if isinstance(r, str)]
|
|
1595
|
+
if not value.get("userId") or "authenticated" not in roles: raise ValueError()
|
|
1596
|
+
return {"id": value["userId"], "loginId": value.get("userDetails", ""), "roles": roles}
|
|
1597
|
+
except Exception: raise AuthError(401, "Invalid client principal")
|
|
1598
|
+
|
|
1599
|
+
def require_roles(user, roles):
|
|
1600
|
+
if roles and not any(role in user["roles"] for role in roles): raise AuthError(403, "Insufficient permissions")
|
|
1601
|
+
|
|
1602
|
+
def handle_auth_error(error):
|
|
1603
|
+
return func.HttpResponse(json.dumps({"error": error.message}), status_code=error.status, mimetype="application/json") if isinstance(error, AuthError) else None
|
|
1604
|
+
`;
|
|
1605
|
+
}
|
|
1606
|
+
function generateSwaAuthHelperCSharp() {
|
|
1607
|
+
return `using System.Text;
|
|
1608
|
+
using System.Text.Json;
|
|
1609
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
1610
|
+
|
|
1611
|
+
namespace Functions.Auth;
|
|
1612
|
+
|
|
1613
|
+
public class JwtPayload
|
|
1614
|
+
{
|
|
1615
|
+
public string Sub { get; set; } = "";
|
|
1616
|
+
public string LoginId { get; set; } = "";
|
|
1617
|
+
public string[] Roles { get; set; } = Array.Empty<string>();
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
public static class JwtHelper
|
|
1621
|
+
{
|
|
1622
|
+
public static async Task<(JwtPayload?, HttpResponseData?)> Authorize(HttpRequestData request, params string[] requiredRoles)
|
|
1623
|
+
{
|
|
1624
|
+
try
|
|
1625
|
+
{
|
|
1626
|
+
var encoded = request.Headers.TryGetValues("x-ms-client-principal", out var values) ? values.FirstOrDefault() : null;
|
|
1627
|
+
if (string.IsNullOrWhiteSpace(encoded)) throw new InvalidOperationException();
|
|
1628
|
+
using var document = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(encoded)));
|
|
1629
|
+
var root = document.RootElement;
|
|
1630
|
+
var userId = root.GetProperty("userId").GetString() ?? "";
|
|
1631
|
+
var details = root.TryGetProperty("userDetails", out var detailValue) ? detailValue.GetString() ?? "" : "";
|
|
1632
|
+
var roles = root.TryGetProperty("userRoles", out var roleValue) && roleValue.ValueKind == JsonValueKind.Array
|
|
1633
|
+
? roleValue.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.String).Select(v => v.GetString()!).ToArray()
|
|
1634
|
+
: Array.Empty<string>();
|
|
1635
|
+
if (string.IsNullOrEmpty(userId) || !roles.Contains("authenticated")) throw new InvalidOperationException();
|
|
1636
|
+
if (requiredRoles.Length > 0 && !requiredRoles.Any(roles.Contains))
|
|
1637
|
+
{
|
|
1638
|
+
var forbidden = request.CreateResponse(System.Net.HttpStatusCode.Forbidden);
|
|
1639
|
+
await forbidden.WriteAsJsonAsync(new { error = "Forbidden" });
|
|
1640
|
+
return (null, forbidden);
|
|
1641
|
+
}
|
|
1642
|
+
return (new JwtPayload { Sub = userId, LoginId = details, Roles = roles }, null);
|
|
1643
|
+
}
|
|
1644
|
+
catch
|
|
1645
|
+
{
|
|
1646
|
+
var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
|
|
1647
|
+
await unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" });
|
|
1648
|
+
return (null, unauthorized);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
`;
|
|
1653
|
+
}
|
|
1193
1654
|
// ============================================================
|
|
1194
1655
|
// 13. scaffold 用ロールガード挿入ヘルパー
|
|
1195
1656
|
// ============================================================
|
|
1196
1657
|
/**
|
|
1197
1658
|
* TypeScript Functions のハンドラーにロールガードを挿入するためのインポート文を生成
|
|
1198
1659
|
*/
|
|
1199
|
-
function generateAuthImportTS() {
|
|
1200
|
-
return `import { requireAuth, requireRoles, handleAuthError } from './auth
|
|
1660
|
+
function generateAuthImportTS(namedPolicy = false) {
|
|
1661
|
+
return `import { requireAuth, requireRoles, handleAuthError } from './auth/${namedPolicy ? "auth-router" : "jwt-helper"}';`;
|
|
1201
1662
|
}
|
|
1202
1663
|
/**
|
|
1203
1664
|
* TypeScript Functions のハンドラー先頭に挿入する認証チェックコードを生成
|
|
1204
1665
|
*/
|
|
1205
|
-
function generateAuthGuardTS(policy, operation) {
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
:
|
|
1666
|
+
function generateAuthGuardTS(policy, operation, provider) {
|
|
1667
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1668
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1669
|
+
const name = typeof selected === 'string' ? selected : policy.policy;
|
|
1670
|
+
return ` const authUser = await requireAuth(request, ${JSON.stringify(name)});`;
|
|
1671
|
+
}
|
|
1672
|
+
const roles = selected || policy.roles || [];
|
|
1209
1673
|
if (roles.length > 0) {
|
|
1210
|
-
return ` const authUser = requireAuth(request);
|
|
1674
|
+
return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);
|
|
1211
1675
|
requireRoles(authUser, ${JSON.stringify(roles)});`;
|
|
1212
1676
|
}
|
|
1213
|
-
return ` const authUser = requireAuth(request);`;
|
|
1677
|
+
return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);`;
|
|
1214
1678
|
}
|
|
1215
1679
|
/**
|
|
1216
1680
|
* C# Functions のハンドラーに挿入するロールガードコードを生成
|
|
1217
1681
|
*/
|
|
1218
1682
|
function generateAuthGuardCSharp(policy, operation) {
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
:
|
|
1683
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1684
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1685
|
+
const name = typeof selected === 'string' ? selected : policy.policy;
|
|
1686
|
+
return ` var (principal, errorResponse) = await AuthRouter.Authorize(request, ${JSON.stringify(name)});\n if (errorResponse != null) return errorResponse;`;
|
|
1687
|
+
}
|
|
1688
|
+
const roles = selected || policy.roles || [];
|
|
1222
1689
|
if (roles.length > 0) {
|
|
1223
1690
|
const rolesStr = roles.map(r => `"${r}"`).join(', ');
|
|
1224
1691
|
return ` var (principal, errorResponse) = await JwtHelper.Authorize(request, ${rolesStr});
|
|
@@ -1231,9 +1698,12 @@ function generateAuthGuardCSharp(policy, operation) {
|
|
|
1231
1698
|
* Python Functions のハンドラーに挿入するロールガードコードを生成
|
|
1232
1699
|
*/
|
|
1233
1700
|
function generateAuthGuardPython(policy, operation) {
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
:
|
|
1701
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1702
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1703
|
+
const name = typeof selected === 'string' ? selected : policy.policy;
|
|
1704
|
+
return ` user = require_auth(req, ${JSON.stringify(name)})`;
|
|
1705
|
+
}
|
|
1706
|
+
const roles = selected || policy.roles || [];
|
|
1237
1707
|
if (roles.length > 0) {
|
|
1238
1708
|
const rolesStr = roles.map(r => `"${r}"`).join(', ');
|
|
1239
1709
|
return ` user = require_auth(req)
|