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
|
@@ -3,10 +3,212 @@
|
|
|
3
3
|
* add-auth コマンドおよび scaffold ロールガード挿入で使用するテンプレート群
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { CustomJwtConfig, ModelAuthPolicy, RdbConnectorConfig } from "../../types";
|
|
6
|
+
import { AuthProvider, CustomJwtConfig, ModelAuthPolicy, NormalizedAuthConfig, RdbConnectorConfig } from "../../types";
|
|
7
7
|
|
|
8
8
|
type RdbProvider = RdbConnectorConfig["provider"]; // "mysql" | "postgres" | "sqlserver"
|
|
9
9
|
|
|
10
|
+
export function generateNamedAuthRouterTS(config: NormalizedAuthConfig): string {
|
|
11
|
+
const external = Object.entries(config.schemes).filter(([, s]) => s.provider === "external-token");
|
|
12
|
+
const custom = Object.entries(config.schemes).filter(([, s]) => s.provider === "custom-jwt");
|
|
13
|
+
const imports = external.map(([name]) => {
|
|
14
|
+
const slug = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
15
|
+
const id = safeIdentifier(name);
|
|
16
|
+
return `import { verifyExternalToken as verify_${id}, ExternalTokenVerificationError as Error_${id} } from './schemes/${slug}/verifier';`;
|
|
17
|
+
}).concat(custom.map(([name]) => {
|
|
18
|
+
const slug = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
19
|
+
return `import { requireAuth as verify_${safeIdentifier(name)} } from './schemes/${slug}/jwt-helper';`;
|
|
20
|
+
})).join("\n");
|
|
21
|
+
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 ");
|
|
22
|
+
return `import type { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
23
|
+
${imports}
|
|
24
|
+
export type AuthPrincipal = { subject: string; scheme: string; issuer: string; roles: string[]; claims: Record<string, unknown>; /** @deprecated */ userId?: string; /** @deprecated */ userDetails?: string };
|
|
25
|
+
export class AuthError extends Error { constructor(public status: 401 | 403 | 503, message: string) { super(message); } }
|
|
26
|
+
const schemes = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n,s]) => [n,{provider:s.provider,allowedProviders:s.swa?.allowedProviders??[]}])))} as const;
|
|
27
|
+
const policies = ${JSON.stringify(config.authorization.policies)} as Record<string, { schemes: string[]; roles?: string[] }>;
|
|
28
|
+
const externalVerifiers: Record<string, (token: string) => Promise<any>> = { ${verifiers} };
|
|
29
|
+
const customVerifiers: Record<string, (request: HttpRequest) => any> = { ${custom.map(([name]) => `${JSON.stringify(name)}: verify_${safeIdentifier(name)}`).join(", ")} };
|
|
30
|
+
function swa(request: HttpRequest, scheme: string): AuthPrincipal {
|
|
31
|
+
const encoded = request.headers.get('x-ms-client-principal');
|
|
32
|
+
if (!encoded) throw new AuthError(401, 'Authentication required');
|
|
33
|
+
try { const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) as Record<string, unknown>;
|
|
34
|
+
const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r): r is string => typeof r === 'string') : [];
|
|
35
|
+
if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
|
|
36
|
+
const allowed = (schemes as any)[scheme]?.allowedProviders as string[] | undefined;
|
|
37
|
+
if (allowed?.length && (typeof value.identityProvider !== 'string' || !allowed.includes(value.identityProvider))) throw new Error();
|
|
38
|
+
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 };
|
|
39
|
+
} catch (e) { if (e instanceof AuthError) throw e; throw new AuthError(401, 'Invalid SWA client principal'); }
|
|
40
|
+
}
|
|
41
|
+
export async function requireAuth(request: HttpRequest, policyName: string): Promise<AuthPrincipal> {
|
|
42
|
+
const policy = policies[policyName]; if (!policy) throw new AuthError(401, 'Undefined authorization policy');
|
|
43
|
+
const bearer = request.headers.get('authorization'); const hasSwa = Boolean(request.headers.get('x-ms-client-principal'));
|
|
44
|
+
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; });
|
|
45
|
+
if (candidates.length !== 1) throw new AuthError(401, candidates.length ? 'Ambiguous credentials' : 'Authentication required');
|
|
46
|
+
const scheme = candidates[0]; const provider = (schemes as any)[scheme]?.provider; let principal: AuthPrincipal;
|
|
47
|
+
if (provider === 'swa') principal = swa(request, scheme);
|
|
48
|
+
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'); } }
|
|
49
|
+
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'); } }
|
|
50
|
+
else throw new AuthError(401, 'Authentication adapter is not configured');
|
|
51
|
+
requireRoles(principal, policy.roles || []); return principal;
|
|
52
|
+
}
|
|
53
|
+
export function requireRoles(principal: AuthPrincipal, roles: string[]): void { if (roles.length && !roles.some(r => principal.roles.includes(r))) throw new AuthError(403, 'Insufficient permissions'); }
|
|
54
|
+
export function identityKey(principal: AuthPrincipal): string { return principal.scheme + ':' + principal.subject; }
|
|
55
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status:error.status, jsonBody:{error:error.message} } : null; }
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeIdentifier(name: string): string {
|
|
60
|
+
return name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function schemeSlug(name: string): string {
|
|
64
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function generateNamedExternalTokenVerifierCSharp(scheme: string): string {
|
|
68
|
+
const id = safeIdentifier(scheme);
|
|
69
|
+
return `namespace Functions.Auth.Schemes;
|
|
70
|
+
public record ExternalPrincipal_${id}(string Subject, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims);
|
|
71
|
+
public sealed class ExternalTokenVerificationException_${id} : Exception
|
|
72
|
+
{
|
|
73
|
+
public bool IsIdentityProviderUnavailable { get; }
|
|
74
|
+
public ExternalTokenVerificationException_${id}(string message, bool unavailable = false) : base(message) => IsIdentityProviderUnavailable = unavailable;
|
|
75
|
+
}
|
|
76
|
+
public static class ExternalTokenVerifier_${id}
|
|
77
|
+
{
|
|
78
|
+
// Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
|
|
79
|
+
public static Task<ExternalPrincipal_${id}> VerifyAsync(string token) => throw new InvalidOperationException("External token verifier for ${scheme} is not configured");
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function generateNamedExternalTokenVerifierPython(scheme: string): string {
|
|
85
|
+
return `class ExternalTokenVerificationError(Exception):
|
|
86
|
+
def __init__(self, message: str, identity_provider_unavailable: bool = False):
|
|
87
|
+
super().__init__(message)
|
|
88
|
+
self.identity_provider_unavailable = identity_provider_unavailable
|
|
89
|
+
|
|
90
|
+
def verify_external_token(token: str) -> dict:
|
|
91
|
+
# Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
|
|
92
|
+
raise RuntimeError("External token verifier for ${scheme} is not configured")
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function generateNamedAuthRouterCSharp(config: NormalizedAuthConfig): string {
|
|
97
|
+
const externalCases = Object.entries(config.schemes).filter(([,s]) => s.provider === "external-token").map(([name]) => {
|
|
98
|
+
const id = safeIdentifier(name);
|
|
99
|
+
return ` case "${name}":
|
|
100
|
+
try
|
|
101
|
+
{
|
|
102
|
+
var external = await ExternalTokenVerifier_${id}.VerifyAsync(bearer!);
|
|
103
|
+
principal = new AuthPrincipal(external.Subject, "${name}", external.Issuer, external.Roles, external.Claims);
|
|
104
|
+
}
|
|
105
|
+
catch (ExternalTokenVerificationException_${id} ex) when (ex.IsIdentityProviderUnavailable) { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "Identity provider unavailable")); }
|
|
106
|
+
catch (ExternalTokenVerificationException_${id}) { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid token")); }
|
|
107
|
+
catch { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "External token verifier unavailable")); }
|
|
108
|
+
break;`;
|
|
109
|
+
}).join("\n");
|
|
110
|
+
return `using System.Net;
|
|
111
|
+
using System.Text;
|
|
112
|
+
using System.Text.Json;
|
|
113
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
114
|
+
using Functions.Auth.Schemes;
|
|
115
|
+
namespace Functions.Auth;
|
|
116
|
+
public record AuthPrincipal(string Subject, string Scheme, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims)
|
|
117
|
+
{
|
|
118
|
+
[Obsolete("Use Subject")] public string UserId => Subject;
|
|
119
|
+
public string IdentityKey => $"{Scheme}:{Subject}";
|
|
120
|
+
}
|
|
121
|
+
public static class AuthRouter
|
|
122
|
+
{
|
|
123
|
+
private static readonly Dictionary<string, (string[] Schemes, string[] Roles)> Policies = new()
|
|
124
|
+
{
|
|
125
|
+
${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")}
|
|
126
|
+
};
|
|
127
|
+
private static readonly Dictionary<string,string> Providers = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]="${s.provider}"`).join(", ")} };
|
|
128
|
+
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(", ")} };
|
|
129
|
+
public static async Task<(AuthPrincipal? Principal, HttpResponseData? ErrorResponse)> Authorize(HttpRequestData request, string policyName)
|
|
130
|
+
{
|
|
131
|
+
if (!Policies.TryGetValue(policyName, out var policy)) return (null, await Error(request, HttpStatusCode.Unauthorized, "Undefined authorization policy"));
|
|
132
|
+
var bearerHeader = request.Headers.TryGetValues("Authorization", out var authValues) ? authValues.FirstOrDefault() : null;
|
|
133
|
+
var bearer = bearerHeader?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) == true ? bearerHeader[7..] : null;
|
|
134
|
+
var swa = request.Headers.TryGetValues("x-ms-client-principal", out var swaValues) ? swaValues.FirstOrDefault() : null;
|
|
135
|
+
var candidates = policy.Schemes.Where(s => Providers[s] == "swa" ? swa != null : Providers[s] is "external-token" or "custom-jwt" ? bearer != null : false).ToArray();
|
|
136
|
+
if (candidates.Length != 1) return (null, await Error(request, HttpStatusCode.Unauthorized, candidates.Length == 0 ? "Authentication required" : "Ambiguous credentials"));
|
|
137
|
+
var scheme = candidates[0]; AuthPrincipal principal;
|
|
138
|
+
switch (scheme)
|
|
139
|
+
{
|
|
140
|
+
${externalCases}
|
|
141
|
+
default:
|
|
142
|
+
if (Providers[scheme] != "swa") return (null, await Error(request, HttpStatusCode.Unauthorized, "Authentication adapter is not configured"));
|
|
143
|
+
try
|
|
144
|
+
{
|
|
145
|
+
using var doc = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(swa!)));
|
|
146
|
+
var root = doc.RootElement; var subject = root.GetProperty("userId").GetString();
|
|
147
|
+
var roles = root.TryGetProperty("userRoles", out var rv) ? rv.EnumerateArray().Where(v=>v.ValueKind==JsonValueKind.String).Select(v=>v.GetString()!).ToArray() : Array.Empty<string>();
|
|
148
|
+
if (string.IsNullOrEmpty(subject) || !roles.Contains("authenticated")) throw new InvalidOperationException();
|
|
149
|
+
var identityProvider = root.TryGetProperty("identityProvider", out var iv) ? iv.GetString() : null;
|
|
150
|
+
if (AllowedProviders[scheme].Length > 0 && (identityProvider == null || !AllowedProviders[scheme].Contains(identityProvider))) throw new InvalidOperationException();
|
|
151
|
+
var issuer = identityProvider == null ? "swa" : "swa:" + identityProvider;
|
|
152
|
+
principal = new AuthPrincipal(subject!, scheme, issuer, roles, new Dictionary<string,object?> { ["clientPrincipal"] = root.Clone() });
|
|
153
|
+
}
|
|
154
|
+
catch { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid SWA client principal")); }
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (policy.Roles.Length > 0 && !policy.Roles.Any(principal.Roles.Contains)) return (null, await Error(request, HttpStatusCode.Forbidden, "Insufficient permissions"));
|
|
158
|
+
return (principal, null);
|
|
159
|
+
}
|
|
160
|
+
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; }
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function generateNamedAuthRouterPython(config: NormalizedAuthConfig): string {
|
|
166
|
+
const external = Object.entries(config.schemes).filter(([,s])=>s.provider === "external-token");
|
|
167
|
+
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");
|
|
168
|
+
const cases = external.map(([n], index)=>` ${index === 0 ? "if" : "elif"} scheme == ${JSON.stringify(n)}:
|
|
169
|
+
try:
|
|
170
|
+
value = verify_${safeIdentifier(n)}(bearer)
|
|
171
|
+
except Error_${safeIdentifier(n)} as exc:
|
|
172
|
+
raise AuthError(503 if exc.identity_provider_unavailable else 401, "Identity provider unavailable" if exc.identity_provider_unavailable else "Invalid token")
|
|
173
|
+
except Exception:
|
|
174
|
+
raise AuthError(503, "External token verifier unavailable")
|
|
175
|
+
if not value.get("subject") or not isinstance(value.get("roles"), list): raise AuthError(401, "Invalid external principal")
|
|
176
|
+
principal = {"subject": value["subject"], "scheme": scheme, "issuer": value.get("issuer", scheme), "roles": value["roles"], "claims": value.get("claims", {}), "userId": value["subject"]}`).join("\n");
|
|
177
|
+
return `import base64
|
|
178
|
+
import json
|
|
179
|
+
import azure.functions as func
|
|
180
|
+
${imports}
|
|
181
|
+
POLICIES = ${JSON.stringify(config.authorization.policies)}
|
|
182
|
+
PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n,s])=>[n,s.provider])))}
|
|
183
|
+
ALLOWED_PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n,s])=>[n,s.swa?.allowedProviders??[]])))}
|
|
184
|
+
class AuthError(Exception):
|
|
185
|
+
def __init__(self, status, message): self.status, self.message = status, message
|
|
186
|
+
def require_auth(req: func.HttpRequest, policy_name: str):
|
|
187
|
+
policy = POLICIES.get(policy_name)
|
|
188
|
+
if not policy: raise AuthError(401, "Undefined authorization policy")
|
|
189
|
+
header = req.headers.get("authorization", "")
|
|
190
|
+
bearer = header[7:] if header.lower().startswith("bearer ") else None
|
|
191
|
+
swa = req.headers.get("x-ms-client-principal")
|
|
192
|
+
candidates = [s for s in policy["schemes"] if (PROVIDERS[s] == "swa" and swa) or (PROVIDERS[s] in ("external-token", "custom-jwt") and bearer)]
|
|
193
|
+
if len(candidates) != 1: raise AuthError(401, "Authentication required" if not candidates else "Ambiguous credentials")
|
|
194
|
+
scheme = candidates[0]
|
|
195
|
+
${cases}
|
|
196
|
+
${external.length ? "elif" : "if"} PROVIDERS[scheme] == "swa":
|
|
197
|
+
try:
|
|
198
|
+
value=json.loads(base64.b64decode(swa).decode("utf-8")); roles=[r for r in value.get("userRoles",[]) if isinstance(r,str)]
|
|
199
|
+
if not value.get("userId") or "authenticated" not in roles: raise ValueError()
|
|
200
|
+
if ALLOWED_PROVIDERS[scheme] and value.get("identityProvider") not in ALLOWED_PROVIDERS[scheme]: raise ValueError()
|
|
201
|
+
principal={"subject":value["userId"],"scheme":scheme,"issuer":"swa:"+value.get("identityProvider", "unknown"),"roles":roles,"claims":value,"userId":value["userId"]}
|
|
202
|
+
except Exception: raise AuthError(401,"Invalid SWA client principal")
|
|
203
|
+
else: raise AuthError(401,"Authentication adapter is not configured")
|
|
204
|
+
require_roles(principal, policy.get("roles", [])); return principal
|
|
205
|
+
def require_roles(principal, roles):
|
|
206
|
+
if roles and not any(role in principal.get("roles",[]) for role in roles): raise AuthError(403,"Insufficient permissions")
|
|
207
|
+
def identity_key(principal): return principal["scheme"] + ":" + principal["subject"]
|
|
208
|
+
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
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
10
212
|
// ============================================================
|
|
11
213
|
// 1. shared/models/auth.ts(Zod スキーマ)
|
|
12
214
|
// ============================================================
|
|
@@ -111,7 +313,7 @@ ${getConnection}
|
|
|
111
313
|
app.http('auth-login', {
|
|
112
314
|
methods: ['POST'],
|
|
113
315
|
route: 'auth/login',
|
|
114
|
-
authLevel: '
|
|
316
|
+
authLevel: 'function',
|
|
115
317
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
116
318
|
try {
|
|
117
319
|
const body = LoginRequest.parse(await request.json());
|
|
@@ -169,7 +371,7 @@ app.http('auth-login', {
|
|
|
169
371
|
app.http('auth-me', {
|
|
170
372
|
methods: ['GET'],
|
|
171
373
|
route: 'auth/me',
|
|
172
|
-
authLevel: '
|
|
374
|
+
authLevel: 'function',
|
|
173
375
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
174
376
|
try {
|
|
175
377
|
const user = requireAuth(request);
|
|
@@ -187,7 +389,7 @@ app.http('auth-me', {
|
|
|
187
389
|
app.http('auth-logout', {
|
|
188
390
|
methods: ['POST'],
|
|
189
391
|
route: 'auth/logout',
|
|
190
|
-
authLevel: '
|
|
392
|
+
authLevel: 'function',
|
|
191
393
|
handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
|
|
192
394
|
return { status: 200, jsonBody: { message: 'Logged out' } };
|
|
193
395
|
},
|
|
@@ -312,7 +514,7 @@ namespace Functions.Auth
|
|
|
312
514
|
{
|
|
313
515
|
[Function("auth-login")]
|
|
314
516
|
public async Task<HttpResponseData> Login(
|
|
315
|
-
[HttpTrigger(AuthorizationLevel.
|
|
517
|
+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/login")] HttpRequestData request)
|
|
316
518
|
{
|
|
317
519
|
var body = await JsonSerializer.DeserializeAsync<LoginRequest>(request.Body);
|
|
318
520
|
if (body == null || string.IsNullOrEmpty(body.LoginId) || string.IsNullOrEmpty(body.Password))
|
|
@@ -371,7 +573,7 @@ namespace Functions.Auth
|
|
|
371
573
|
|
|
372
574
|
[Function("auth-me")]
|
|
373
575
|
public async Task<HttpResponseData> Me(
|
|
374
|
-
[HttpTrigger(AuthorizationLevel.
|
|
576
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "auth/me")] HttpRequestData request)
|
|
375
577
|
{
|
|
376
578
|
var (principal, errorResponse) = await JwtHelper.Authorize(request);
|
|
377
579
|
if (errorResponse != null) return errorResponse;
|
|
@@ -390,7 +592,7 @@ namespace Functions.Auth
|
|
|
390
592
|
|
|
391
593
|
[Function("auth-logout")]
|
|
392
594
|
public async Task<HttpResponseData> Logout(
|
|
393
|
-
[HttpTrigger(AuthorizationLevel.
|
|
595
|
+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/logout")] HttpRequestData request)
|
|
394
596
|
{
|
|
395
597
|
var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
|
|
396
598
|
await response.WriteAsJsonAsync(new { message = "Logged out" });
|
|
@@ -580,7 +782,7 @@ auth_bp = func.Blueprint()
|
|
|
580
782
|
${getConnection}
|
|
581
783
|
|
|
582
784
|
|
|
583
|
-
@auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.
|
|
785
|
+
@auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
|
|
584
786
|
def auth_login(req: func.HttpRequest) -> func.HttpResponse:
|
|
585
787
|
try:
|
|
586
788
|
body = req.get_json()
|
|
@@ -652,7 +854,7 @@ def auth_login(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
652
854
|
)
|
|
653
855
|
|
|
654
856
|
|
|
655
|
-
@auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.
|
|
857
|
+
@auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.FUNCTION)
|
|
656
858
|
def auth_me(req: func.HttpRequest) -> func.HttpResponse:
|
|
657
859
|
from auth.jwt_helper import require_auth, handle_auth_error
|
|
658
860
|
|
|
@@ -671,7 +873,7 @@ def auth_me(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
671
873
|
)
|
|
672
874
|
|
|
673
875
|
|
|
674
|
-
@auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.
|
|
876
|
+
@auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
|
|
675
877
|
def auth_logout(req: func.HttpRequest) -> func.HttpResponse:
|
|
676
878
|
return func.HttpResponse(
|
|
677
879
|
json.dumps({"message": "Logged out"}),
|
|
@@ -1122,6 +1324,17 @@ function getFunctionsBaseUrl(): string {
|
|
|
1122
1324
|
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
1123
1325
|
}
|
|
1124
1326
|
|
|
1327
|
+
function isLocalFunctionsUrl(url: string): boolean {
|
|
1328
|
+
return url.startsWith('http://localhost:') || url.startsWith('http://127.0.0.1:');
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
1332
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
1333
|
+
if (key) return { 'x-functions-key': key };
|
|
1334
|
+
if (isLocalFunctionsUrl(functionsBaseUrl)) return {};
|
|
1335
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1125
1338
|
interface CallFunctionConfig<TInput = any, TOutput = any> {
|
|
1126
1339
|
/** HTTP メソッド */
|
|
1127
1340
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
@@ -1160,11 +1373,13 @@ export async function callFunction<TInput = any, TOutput = any>(
|
|
|
1160
1373
|
// Azure Functions を呼び出し
|
|
1161
1374
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
1162
1375
|
const url = functionsBaseUrl + path;
|
|
1376
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
1163
1377
|
console.log(\`[BFF] \${method} \${url}\`);
|
|
1164
1378
|
|
|
1165
1379
|
// Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)
|
|
1166
1380
|
const fetchHeaders: Record<string, string> = {
|
|
1167
1381
|
'Content-Type': 'application/json',
|
|
1382
|
+
...keyHeaders,
|
|
1168
1383
|
};
|
|
1169
1384
|
try {
|
|
1170
1385
|
const reqHeaders = await headers();
|
|
@@ -1224,6 +1439,258 @@ export async function callFunction<TInput = any, TOutput = any>(
|
|
|
1224
1439
|
`;
|
|
1225
1440
|
}
|
|
1226
1441
|
|
|
1442
|
+
/** SWA built-in auth variant: forward only a platform-derived client principal. */
|
|
1443
|
+
export function generateBFFCallFunctionWithSwaAuth(): string {
|
|
1444
|
+
return generateBFFCallFunctionWithAuth()
|
|
1445
|
+
.replace(
|
|
1446
|
+
` // Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)`,
|
|
1447
|
+
` // SWA client principal is accepted only from the platform request context.`
|
|
1448
|
+
)
|
|
1449
|
+
.replace(
|
|
1450
|
+
` const authorization = reqHeaders.get('authorization');
|
|
1451
|
+
if (authorization) {
|
|
1452
|
+
fetchHeaders['Authorization'] = authorization;
|
|
1453
|
+
}`,
|
|
1454
|
+
` // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.
|
|
1455
|
+
let principal: string | null = null;
|
|
1456
|
+
{
|
|
1457
|
+
const cookie = reqHeaders.get('cookie');
|
|
1458
|
+
const host = reqHeaders.get('host');
|
|
1459
|
+
if (cookie && host) {
|
|
1460
|
+
const proto = reqHeaders.get('x-forwarded-proto') || 'https';
|
|
1461
|
+
const meResponse = await fetch(\`\${proto}://\${host}/.auth/me\`, {
|
|
1462
|
+
headers: { cookie },
|
|
1463
|
+
cache: 'no-store',
|
|
1464
|
+
});
|
|
1465
|
+
if (meResponse.ok) {
|
|
1466
|
+
const payload = await meResponse.json() as { clientPrincipal?: unknown };
|
|
1467
|
+
if (payload.clientPrincipal) {
|
|
1468
|
+
principal = Buffer.from(JSON.stringify(payload.clientPrincipal), 'utf8').toString('base64');
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (principal) {
|
|
1474
|
+
const parsed = JSON.parse(Buffer.from(principal, 'base64').toString('utf8'));
|
|
1475
|
+
const normalized = {
|
|
1476
|
+
identityProvider: String(parsed.identityProvider || ''),
|
|
1477
|
+
userId: String(parsed.userId || ''),
|
|
1478
|
+
userDetails: String(parsed.userDetails || ''),
|
|
1479
|
+
userRoles: Array.isArray(parsed.userRoles) ? parsed.userRoles.filter((role: unknown) => typeof role === 'string') : [],
|
|
1480
|
+
};
|
|
1481
|
+
if (normalized.userId && normalized.userRoles.includes('authenticated')) {
|
|
1482
|
+
fetchHeaders['x-ms-client-principal'] = Buffer.from(JSON.stringify(normalized), 'utf8').toString('base64');
|
|
1483
|
+
}
|
|
1484
|
+
}`
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/** Combined transport for named schemes. Functions still performs all verification. */
|
|
1489
|
+
export function generateBFFCallFunctionWithMultipleAuth(): string {
|
|
1490
|
+
return generateBFFCallFunctionWithSwaAuth().replace(
|
|
1491
|
+
` // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.`,
|
|
1492
|
+
` const authorization = reqHeaders.get('authorization');
|
|
1493
|
+
if (authorization?.startsWith('Bearer ')) fetchHeaders['Authorization'] = authorization;
|
|
1494
|
+
// Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
export function generateSwaAuthHelperTS(): string {
|
|
1499
|
+
return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1500
|
+
|
|
1501
|
+
export interface AuthUser { id: string; loginId: string; roles: string[]; }
|
|
1502
|
+
export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
|
|
1503
|
+
|
|
1504
|
+
export function requireAuth(request: HttpRequest): AuthUser {
|
|
1505
|
+
const encoded = request.headers.get('x-ms-client-principal');
|
|
1506
|
+
if (!encoded) throw new AuthError(401, 'Authentication required');
|
|
1507
|
+
try {
|
|
1508
|
+
const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
|
|
1509
|
+
const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r: unknown) => typeof r === 'string') : [];
|
|
1510
|
+
if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
|
|
1511
|
+
return { id: value.userId, loginId: typeof value.userDetails === 'string' ? value.userDetails : '', roles };
|
|
1512
|
+
} catch { throw new AuthError(401, 'Invalid client principal'); }
|
|
1513
|
+
}
|
|
1514
|
+
export function requireRoles(user: AuthUser, roles: string[]): void {
|
|
1515
|
+
if (roles.length && !roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions');
|
|
1516
|
+
}
|
|
1517
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null {
|
|
1518
|
+
return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null;
|
|
1519
|
+
}
|
|
1520
|
+
`;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// ============================================================
|
|
1524
|
+
// External token authentication
|
|
1525
|
+
// ============================================================
|
|
1526
|
+
|
|
1527
|
+
export function generateExternalTokenAdapter(): string {
|
|
1528
|
+
return `/** Replace these functions with your external IdP SDK integration. */
|
|
1529
|
+
export async function getToken(): Promise<string | null> { return null; }
|
|
1530
|
+
export async function login(): Promise<void> { throw new Error('External token login adapter is not configured'); }
|
|
1531
|
+
export async function logout(): Promise<void> { throw new Error('External token logout adapter is not configured'); }
|
|
1532
|
+
`;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
export function generateAuthenticatedFetch(): string {
|
|
1536
|
+
return `import { getToken } from './external-token-adapter';
|
|
1537
|
+
|
|
1538
|
+
export async function authenticatedFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
|
|
1539
|
+
const token = await getToken();
|
|
1540
|
+
if (!token) return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'content-type': 'application/json' } });
|
|
1541
|
+
const headers = new Headers(init.headers);
|
|
1542
|
+
headers.set('Authorization', \`Bearer \${token}\`);
|
|
1543
|
+
return fetch(input, { ...init, headers });
|
|
1544
|
+
}
|
|
1545
|
+
`;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
export function generateExternalTokenBFFMeRoute(): string {
|
|
1549
|
+
return `import { callFunction } from '@/lib/api/call-function';
|
|
1550
|
+
export async function GET() { return callFunction({ method: 'GET', path: '/api/auth/me' }); }
|
|
1551
|
+
`;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
export function generateExternalTokenAuthContext(): string {
|
|
1555
|
+
return `'use client';
|
|
1556
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
|
1557
|
+
import { authenticatedFetch } from './authenticated-fetch';
|
|
1558
|
+
import * as adapter from './external-token-adapter';
|
|
1559
|
+
export interface AuthUser { id: string; loginId: string; name: string; email: string; roles: string[]; }
|
|
1560
|
+
interface Value { user: AuthUser | null; loading: boolean; login: () => Promise<void>; logout: () => Promise<void>; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; }
|
|
1561
|
+
const Context = createContext<Value | null>(null);
|
|
1562
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
1563
|
+
const [user, setUser] = useState<AuthUser | null>(null); const [loading, setLoading] = useState(true);
|
|
1564
|
+
useEffect(() => { authenticatedFetch('/api/auth/me').then(async r => {
|
|
1565
|
+
if (!r.ok || !(r.headers.get('content-type') || '').includes('application/json')) return null;
|
|
1566
|
+
return r.json() as Promise<AuthUser>;
|
|
1567
|
+
}).then(setUser).catch(() => setUser(null)).finally(() => setLoading(false)); }, []);
|
|
1568
|
+
const hasRole = (role: string) => user?.roles.includes(role) ?? false;
|
|
1569
|
+
const logout = async () => { await adapter.logout(); setUser(null); };
|
|
1570
|
+
return <Context.Provider value={{ user, loading, login: adapter.login, logout, hasRole, hasAnyRole: roles => roles.some(hasRole) }}>{children}</Context.Provider>;
|
|
1571
|
+
}
|
|
1572
|
+
export function useAuth() { const value = useContext(Context); if (!value) throw new Error('useAuth must be used within an AuthProvider'); return value; }
|
|
1573
|
+
`;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
export function generateExternalTokenVerifierTS(): string {
|
|
1577
|
+
return `export interface ExternalAuthPrincipal { userId: string; userDetails: string; roles: string[]; }
|
|
1578
|
+
export class ExternalTokenVerificationError extends Error { constructor(public status: 401 | 503, message: string) { super(message); } }
|
|
1579
|
+
/** Implement IdP verification here. Never decode and trust a token without verification. */
|
|
1580
|
+
export async function verifyExternalToken(_token: string): Promise<ExternalAuthPrincipal> {
|
|
1581
|
+
throw new Error('External token verifier is not configured');
|
|
1582
|
+
}
|
|
1583
|
+
`;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
export function generateExternalTokenHelperTS(): string {
|
|
1587
|
+
return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1588
|
+
import { verifyExternalToken, ExternalTokenVerificationError, type ExternalAuthPrincipal } from './external-token-verifier';
|
|
1589
|
+
export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
|
|
1590
|
+
export async function requireAuth(request: HttpRequest): Promise<ExternalAuthPrincipal> {
|
|
1591
|
+
const header = request.headers.get('authorization') || '';
|
|
1592
|
+
if (!header.startsWith('Bearer ') || !header.slice(7)) throw new AuthError(401, 'Missing or invalid Authorization header');
|
|
1593
|
+
try { const principal = await verifyExternalToken(header.slice(7));
|
|
1594
|
+
if (!principal?.userId || !Array.isArray(principal.roles)) throw new AuthError(401, 'Invalid external principal');
|
|
1595
|
+
return principal;
|
|
1596
|
+
} 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'); }
|
|
1597
|
+
}
|
|
1598
|
+
export function requireRoles(user: ExternalAuthPrincipal, roles: string[]) { if (!roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions'); }
|
|
1599
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null; }
|
|
1600
|
+
`;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
export function generateExternalAuthMeTS(): string {
|
|
1604
|
+
return `import { app, HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1605
|
+
import { requireAuth, handleAuthError } from './auth/jwt-helper';
|
|
1606
|
+
app.http('external-auth-me', { methods: ['GET'], route: 'auth/me', authLevel: 'function', handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
|
|
1607
|
+
try { const p = await requireAuth(request); return { status: 200, jsonBody: { id: p.userId, loginId: p.userDetails, name: p.userDetails, email: '', roles: p.roles } }; }
|
|
1608
|
+
catch (error) { return handleAuthError(error) || { status: 500, jsonBody: { error: 'Authentication failed' } }; }
|
|
1609
|
+
} });
|
|
1610
|
+
`;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
export function generateExternalTokenVerifierCSharp(): string { 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`; }
|
|
1614
|
+
export function generateExternalTokenHelperCSharp(): string { 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`; }
|
|
1615
|
+
export function generateExternalAuthMeCSharp(): string { 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`; }
|
|
1616
|
+
export function generateExternalTokenVerifierPython(): string { 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`; }
|
|
1617
|
+
export function generateExternalTokenHelperPython(): string { 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`; }
|
|
1618
|
+
export function generateExternalAuthMePython(): string { 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`; }
|
|
1619
|
+
export function generateSwaAuthHelperPython(): string {
|
|
1620
|
+
return `import base64
|
|
1621
|
+
import json
|
|
1622
|
+
import azure.functions as func
|
|
1623
|
+
|
|
1624
|
+
class AuthError(Exception):
|
|
1625
|
+
def __init__(self, status, message): self.status, self.message = status, message
|
|
1626
|
+
|
|
1627
|
+
def require_auth(req):
|
|
1628
|
+
encoded = req.headers.get("x-ms-client-principal")
|
|
1629
|
+
if not encoded: raise AuthError(401, "Authentication required")
|
|
1630
|
+
try:
|
|
1631
|
+
value = json.loads(base64.b64decode(encoded).decode("utf-8"))
|
|
1632
|
+
roles = [r for r in value.get("userRoles", []) if isinstance(r, str)]
|
|
1633
|
+
if not value.get("userId") or "authenticated" not in roles: raise ValueError()
|
|
1634
|
+
return {"id": value["userId"], "loginId": value.get("userDetails", ""), "roles": roles}
|
|
1635
|
+
except Exception: raise AuthError(401, "Invalid client principal")
|
|
1636
|
+
|
|
1637
|
+
def require_roles(user, roles):
|
|
1638
|
+
if roles and not any(role in user["roles"] for role in roles): raise AuthError(403, "Insufficient permissions")
|
|
1639
|
+
|
|
1640
|
+
def handle_auth_error(error):
|
|
1641
|
+
return func.HttpResponse(json.dumps({"error": error.message}), status_code=error.status, mimetype="application/json") if isinstance(error, AuthError) else None
|
|
1642
|
+
`;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
export function generateSwaAuthHelperCSharp(): string {
|
|
1646
|
+
return `using System.Text;
|
|
1647
|
+
using System.Text.Json;
|
|
1648
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
1649
|
+
|
|
1650
|
+
namespace Functions.Auth;
|
|
1651
|
+
|
|
1652
|
+
public class JwtPayload
|
|
1653
|
+
{
|
|
1654
|
+
public string Sub { get; set; } = "";
|
|
1655
|
+
public string LoginId { get; set; } = "";
|
|
1656
|
+
public string[] Roles { get; set; } = Array.Empty<string>();
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
public static class JwtHelper
|
|
1660
|
+
{
|
|
1661
|
+
public static async Task<(JwtPayload?, HttpResponseData?)> Authorize(HttpRequestData request, params string[] requiredRoles)
|
|
1662
|
+
{
|
|
1663
|
+
try
|
|
1664
|
+
{
|
|
1665
|
+
var encoded = request.Headers.TryGetValues("x-ms-client-principal", out var values) ? values.FirstOrDefault() : null;
|
|
1666
|
+
if (string.IsNullOrWhiteSpace(encoded)) throw new InvalidOperationException();
|
|
1667
|
+
using var document = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(encoded)));
|
|
1668
|
+
var root = document.RootElement;
|
|
1669
|
+
var userId = root.GetProperty("userId").GetString() ?? "";
|
|
1670
|
+
var details = root.TryGetProperty("userDetails", out var detailValue) ? detailValue.GetString() ?? "" : "";
|
|
1671
|
+
var roles = root.TryGetProperty("userRoles", out var roleValue) && roleValue.ValueKind == JsonValueKind.Array
|
|
1672
|
+
? roleValue.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.String).Select(v => v.GetString()!).ToArray()
|
|
1673
|
+
: Array.Empty<string>();
|
|
1674
|
+
if (string.IsNullOrEmpty(userId) || !roles.Contains("authenticated")) throw new InvalidOperationException();
|
|
1675
|
+
if (requiredRoles.Length > 0 && !requiredRoles.Any(roles.Contains))
|
|
1676
|
+
{
|
|
1677
|
+
var forbidden = request.CreateResponse(System.Net.HttpStatusCode.Forbidden);
|
|
1678
|
+
await forbidden.WriteAsJsonAsync(new { error = "Forbidden" });
|
|
1679
|
+
return (null, forbidden);
|
|
1680
|
+
}
|
|
1681
|
+
return (new JwtPayload { Sub = userId, LoginId = details, Roles = roles }, null);
|
|
1682
|
+
}
|
|
1683
|
+
catch
|
|
1684
|
+
{
|
|
1685
|
+
var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
|
|
1686
|
+
await unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" });
|
|
1687
|
+
return (null, unauthorized);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
`;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1227
1694
|
// ============================================================
|
|
1228
1695
|
// 13. scaffold 用ロールガード挿入ヘルパー
|
|
1229
1696
|
// ============================================================
|
|
@@ -1231,32 +1698,38 @@ export async function callFunction<TInput = any, TOutput = any>(
|
|
|
1231
1698
|
/**
|
|
1232
1699
|
* TypeScript Functions のハンドラーにロールガードを挿入するためのインポート文を生成
|
|
1233
1700
|
*/
|
|
1234
|
-
export function generateAuthImportTS(): string {
|
|
1235
|
-
return `import { requireAuth, requireRoles, handleAuthError } from './auth
|
|
1701
|
+
export function generateAuthImportTS(namedPolicy = false): string {
|
|
1702
|
+
return `import { requireAuth, requireRoles, handleAuthError } from './auth/${namedPolicy ? "auth-router" : "jwt-helper"}';`;
|
|
1236
1703
|
}
|
|
1237
1704
|
|
|
1238
1705
|
/**
|
|
1239
1706
|
* TypeScript Functions のハンドラー先頭に挿入する認証チェックコードを生成
|
|
1240
1707
|
*/
|
|
1241
|
-
export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
:
|
|
1708
|
+
export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write', provider?: AuthProvider): string {
|
|
1709
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1710
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1711
|
+
const name = typeof selected === 'string' ? selected : policy.policy!;
|
|
1712
|
+
return ` const authUser = await requireAuth(request, ${JSON.stringify(name)});`;
|
|
1713
|
+
}
|
|
1714
|
+
const roles = selected || policy.roles || [];
|
|
1245
1715
|
|
|
1246
1716
|
if (roles.length > 0) {
|
|
1247
|
-
return ` const authUser = requireAuth(request);
|
|
1717
|
+
return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);
|
|
1248
1718
|
requireRoles(authUser, ${JSON.stringify(roles)});`;
|
|
1249
1719
|
}
|
|
1250
|
-
return ` const authUser = requireAuth(request);`;
|
|
1720
|
+
return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);`;
|
|
1251
1721
|
}
|
|
1252
1722
|
|
|
1253
1723
|
/**
|
|
1254
1724
|
* C# Functions のハンドラーに挿入するロールガードコードを生成
|
|
1255
1725
|
*/
|
|
1256
1726
|
export function generateAuthGuardCSharp(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
:
|
|
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 ` var (principal, errorResponse) = await AuthRouter.Authorize(request, ${JSON.stringify(name)});\n if (errorResponse != null) return errorResponse;`;
|
|
1731
|
+
}
|
|
1732
|
+
const roles = selected || policy.roles || [];
|
|
1260
1733
|
|
|
1261
1734
|
if (roles.length > 0) {
|
|
1262
1735
|
const rolesStr = roles.map(r => `"${r}"`).join(', ');
|
|
@@ -1271,9 +1744,12 @@ export function generateAuthGuardCSharp(policy: ModelAuthPolicy, operation: 'rea
|
|
|
1271
1744
|
* Python Functions のハンドラーに挿入するロールガードコードを生成
|
|
1272
1745
|
*/
|
|
1273
1746
|
export function generateAuthGuardPython(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
:
|
|
1747
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1748
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1749
|
+
const name = typeof selected === 'string' ? selected : policy.policy!;
|
|
1750
|
+
return ` user = require_auth(req, ${JSON.stringify(name)})`;
|
|
1751
|
+
}
|
|
1752
|
+
const roles = selected || policy.roles || [];
|
|
1277
1753
|
|
|
1278
1754
|
if (roles.length > 0) {
|
|
1279
1755
|
const rolesStr = roles.map(r => `"${r}"`).join(', ');
|