swallowkit 1.0.0-beta.45 → 1.0.0-beta.47
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 +2 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +214 -16
- 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 +100 -12
- 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 +74 -30
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +37 -5
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts +20 -2
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +265 -12
- 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 +19 -19
- 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 +16 -16
- package/dist/core/scaffold/functions-generator.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/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.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 +103 -0
- package/src/__tests__/dev.test.ts +27 -0
- package/src/__tests__/init.test.ts +27 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/cli/commands/add-auth.ts +239 -16
- package/src/cli/commands/dev.ts +111 -14
- package/src/cli/commands/init.ts +76 -30
- package/src/cli/commands/scaffold.ts +37 -5
- package/src/cli/index.ts +3 -1
- package/src/core/scaffold/auth-generator.ts +267 -13
- package/src/core/scaffold/connector-functions-generator.ts +22 -19
- package/src/core/scaffold/functions-generator.ts +17 -17
- package/src/core/scaffold/nextjs-generator.ts +13 -1
- package/src/types/index.ts +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* add-auth コマンドおよび scaffold ロールガード挿入で使用するテンプレート群
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { CustomJwtConfig, ModelAuthPolicy, RdbConnectorConfig } from "../../types";
|
|
6
|
+
import { AuthProvider, CustomJwtConfig, ModelAuthPolicy, RdbConnectorConfig } from "../../types";
|
|
7
7
|
|
|
8
8
|
type RdbProvider = RdbConnectorConfig["provider"]; // "mysql" | "postgres" | "sqlserver"
|
|
9
9
|
|
|
@@ -111,7 +111,7 @@ ${getConnection}
|
|
|
111
111
|
app.http('auth-login', {
|
|
112
112
|
methods: ['POST'],
|
|
113
113
|
route: 'auth/login',
|
|
114
|
-
authLevel: '
|
|
114
|
+
authLevel: 'function',
|
|
115
115
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
116
116
|
try {
|
|
117
117
|
const body = LoginRequest.parse(await request.json());
|
|
@@ -169,7 +169,7 @@ app.http('auth-login', {
|
|
|
169
169
|
app.http('auth-me', {
|
|
170
170
|
methods: ['GET'],
|
|
171
171
|
route: 'auth/me',
|
|
172
|
-
authLevel: '
|
|
172
|
+
authLevel: 'function',
|
|
173
173
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
174
174
|
try {
|
|
175
175
|
const user = requireAuth(request);
|
|
@@ -187,7 +187,7 @@ app.http('auth-me', {
|
|
|
187
187
|
app.http('auth-logout', {
|
|
188
188
|
methods: ['POST'],
|
|
189
189
|
route: 'auth/logout',
|
|
190
|
-
authLevel: '
|
|
190
|
+
authLevel: 'function',
|
|
191
191
|
handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
|
|
192
192
|
return { status: 200, jsonBody: { message: 'Logged out' } };
|
|
193
193
|
},
|
|
@@ -312,7 +312,7 @@ namespace Functions.Auth
|
|
|
312
312
|
{
|
|
313
313
|
[Function("auth-login")]
|
|
314
314
|
public async Task<HttpResponseData> Login(
|
|
315
|
-
[HttpTrigger(AuthorizationLevel.
|
|
315
|
+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/login")] HttpRequestData request)
|
|
316
316
|
{
|
|
317
317
|
var body = await JsonSerializer.DeserializeAsync<LoginRequest>(request.Body);
|
|
318
318
|
if (body == null || string.IsNullOrEmpty(body.LoginId) || string.IsNullOrEmpty(body.Password))
|
|
@@ -371,7 +371,7 @@ namespace Functions.Auth
|
|
|
371
371
|
|
|
372
372
|
[Function("auth-me")]
|
|
373
373
|
public async Task<HttpResponseData> Me(
|
|
374
|
-
[HttpTrigger(AuthorizationLevel.
|
|
374
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "auth/me")] HttpRequestData request)
|
|
375
375
|
{
|
|
376
376
|
var (principal, errorResponse) = await JwtHelper.Authorize(request);
|
|
377
377
|
if (errorResponse != null) return errorResponse;
|
|
@@ -390,7 +390,7 @@ namespace Functions.Auth
|
|
|
390
390
|
|
|
391
391
|
[Function("auth-logout")]
|
|
392
392
|
public async Task<HttpResponseData> Logout(
|
|
393
|
-
[HttpTrigger(AuthorizationLevel.
|
|
393
|
+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/logout")] HttpRequestData request)
|
|
394
394
|
{
|
|
395
395
|
var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
|
|
396
396
|
await response.WriteAsJsonAsync(new { message = "Logged out" });
|
|
@@ -580,7 +580,7 @@ auth_bp = func.Blueprint()
|
|
|
580
580
|
${getConnection}
|
|
581
581
|
|
|
582
582
|
|
|
583
|
-
@auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.
|
|
583
|
+
@auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
|
|
584
584
|
def auth_login(req: func.HttpRequest) -> func.HttpResponse:
|
|
585
585
|
try:
|
|
586
586
|
body = req.get_json()
|
|
@@ -652,7 +652,7 @@ def auth_login(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
652
652
|
)
|
|
653
653
|
|
|
654
654
|
|
|
655
|
-
@auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.
|
|
655
|
+
@auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.FUNCTION)
|
|
656
656
|
def auth_me(req: func.HttpRequest) -> func.HttpResponse:
|
|
657
657
|
from auth.jwt_helper import require_auth, handle_auth_error
|
|
658
658
|
|
|
@@ -671,7 +671,7 @@ def auth_me(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
671
671
|
)
|
|
672
672
|
|
|
673
673
|
|
|
674
|
-
@auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.
|
|
674
|
+
@auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
|
|
675
675
|
def auth_logout(req: func.HttpRequest) -> func.HttpResponse:
|
|
676
676
|
return func.HttpResponse(
|
|
677
677
|
json.dumps({"message": "Logged out"}),
|
|
@@ -1122,6 +1122,17 @@ function getFunctionsBaseUrl(): string {
|
|
|
1122
1122
|
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
1123
1123
|
}
|
|
1124
1124
|
|
|
1125
|
+
function isLocalFunctionsUrl(url: string): boolean {
|
|
1126
|
+
return url.startsWith('http://localhost:') || url.startsWith('http://127.0.0.1:');
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
1130
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
1131
|
+
if (key) return { 'x-functions-key': key };
|
|
1132
|
+
if (isLocalFunctionsUrl(functionsBaseUrl)) return {};
|
|
1133
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1125
1136
|
interface CallFunctionConfig<TInput = any, TOutput = any> {
|
|
1126
1137
|
/** HTTP メソッド */
|
|
1127
1138
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
@@ -1160,11 +1171,13 @@ export async function callFunction<TInput = any, TOutput = any>(
|
|
|
1160
1171
|
// Azure Functions を呼び出し
|
|
1161
1172
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
1162
1173
|
const url = functionsBaseUrl + path;
|
|
1174
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
1163
1175
|
console.log(\`[BFF] \${method} \${url}\`);
|
|
1164
1176
|
|
|
1165
1177
|
// Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)
|
|
1166
1178
|
const fetchHeaders: Record<string, string> = {
|
|
1167
1179
|
'Content-Type': 'application/json',
|
|
1180
|
+
...keyHeaders,
|
|
1168
1181
|
};
|
|
1169
1182
|
try {
|
|
1170
1183
|
const reqHeaders = await headers();
|
|
@@ -1224,6 +1237,247 @@ export async function callFunction<TInput = any, TOutput = any>(
|
|
|
1224
1237
|
`;
|
|
1225
1238
|
}
|
|
1226
1239
|
|
|
1240
|
+
/** SWA built-in auth variant: forward only a platform-derived client principal. */
|
|
1241
|
+
export function generateBFFCallFunctionWithSwaAuth(): string {
|
|
1242
|
+
return generateBFFCallFunctionWithAuth()
|
|
1243
|
+
.replace(
|
|
1244
|
+
` // Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)`,
|
|
1245
|
+
` // SWA client principal is accepted only from the platform request context.`
|
|
1246
|
+
)
|
|
1247
|
+
.replace(
|
|
1248
|
+
` const authorization = reqHeaders.get('authorization');
|
|
1249
|
+
if (authorization) {
|
|
1250
|
+
fetchHeaders['Authorization'] = authorization;
|
|
1251
|
+
}`,
|
|
1252
|
+
` let principal = reqHeaders.get('x-ms-client-principal');
|
|
1253
|
+
if (!principal) {
|
|
1254
|
+
const cookie = reqHeaders.get('cookie');
|
|
1255
|
+
const host = reqHeaders.get('host');
|
|
1256
|
+
if (cookie && host) {
|
|
1257
|
+
const proto = reqHeaders.get('x-forwarded-proto') || 'https';
|
|
1258
|
+
const meResponse = await fetch(\`\${proto}://\${host}/.auth/me\`, {
|
|
1259
|
+
headers: { cookie },
|
|
1260
|
+
cache: 'no-store',
|
|
1261
|
+
});
|
|
1262
|
+
if (meResponse.ok) {
|
|
1263
|
+
const payload = await meResponse.json() as { clientPrincipal?: unknown };
|
|
1264
|
+
if (payload.clientPrincipal) {
|
|
1265
|
+
principal = Buffer.from(JSON.stringify(payload.clientPrincipal), 'utf8').toString('base64');
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (principal) {
|
|
1271
|
+
const parsed = JSON.parse(Buffer.from(principal, 'base64').toString('utf8'));
|
|
1272
|
+
const normalized = {
|
|
1273
|
+
identityProvider: String(parsed.identityProvider || ''),
|
|
1274
|
+
userId: String(parsed.userId || ''),
|
|
1275
|
+
userDetails: String(parsed.userDetails || ''),
|
|
1276
|
+
userRoles: Array.isArray(parsed.userRoles) ? parsed.userRoles.filter((role: unknown) => typeof role === 'string') : [],
|
|
1277
|
+
};
|
|
1278
|
+
if (normalized.userId && normalized.userRoles.includes('authenticated')) {
|
|
1279
|
+
fetchHeaders['x-ms-client-principal'] = Buffer.from(JSON.stringify(normalized), 'utf8').toString('base64');
|
|
1280
|
+
}
|
|
1281
|
+
}`
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
export function generateSwaAuthHelperTS(): string {
|
|
1286
|
+
return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1287
|
+
|
|
1288
|
+
export interface AuthUser { id: string; loginId: string; roles: string[]; }
|
|
1289
|
+
export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
|
|
1290
|
+
|
|
1291
|
+
export function requireAuth(request: HttpRequest): AuthUser {
|
|
1292
|
+
const encoded = request.headers.get('x-ms-client-principal');
|
|
1293
|
+
if (!encoded) throw new AuthError(401, 'Authentication required');
|
|
1294
|
+
try {
|
|
1295
|
+
const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
|
|
1296
|
+
const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r: unknown) => typeof r === 'string') : [];
|
|
1297
|
+
if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
|
|
1298
|
+
return { id: value.userId, loginId: typeof value.userDetails === 'string' ? value.userDetails : '', roles };
|
|
1299
|
+
} catch { throw new AuthError(401, 'Invalid client principal'); }
|
|
1300
|
+
}
|
|
1301
|
+
export function requireRoles(user: AuthUser, roles: string[]): void {
|
|
1302
|
+
if (roles.length && !roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions');
|
|
1303
|
+
}
|
|
1304
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null {
|
|
1305
|
+
return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null;
|
|
1306
|
+
}
|
|
1307
|
+
`;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// ============================================================
|
|
1311
|
+
// External token authentication
|
|
1312
|
+
// ============================================================
|
|
1313
|
+
|
|
1314
|
+
export function generateExternalTokenAdapter(): string {
|
|
1315
|
+
return `/** Replace these functions with your external IdP SDK integration. */
|
|
1316
|
+
export async function getToken(): Promise<string | null> { return null; }
|
|
1317
|
+
export async function login(): Promise<void> { throw new Error('External token login adapter is not configured'); }
|
|
1318
|
+
export async function logout(): Promise<void> { throw new Error('External token logout adapter is not configured'); }
|
|
1319
|
+
`;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
export function generateAuthenticatedFetch(): string {
|
|
1323
|
+
return `import { getToken } from './external-token-adapter';
|
|
1324
|
+
|
|
1325
|
+
export async function authenticatedFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
|
|
1326
|
+
const token = await getToken();
|
|
1327
|
+
if (!token) return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'content-type': 'application/json' } });
|
|
1328
|
+
const headers = new Headers(init.headers);
|
|
1329
|
+
headers.set('Authorization', \`Bearer \${token}\`);
|
|
1330
|
+
return fetch(input, { ...init, headers });
|
|
1331
|
+
}
|
|
1332
|
+
`;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
export function generateExternalTokenBFFMeRoute(): string {
|
|
1336
|
+
return `import { callFunction } from '@/lib/api/call-function';
|
|
1337
|
+
export async function GET() { return callFunction({ method: 'GET', path: '/api/auth/me' }); }
|
|
1338
|
+
`;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
export function generateExternalTokenAuthContext(): string {
|
|
1342
|
+
return `'use client';
|
|
1343
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
|
1344
|
+
import { authenticatedFetch } from './authenticated-fetch';
|
|
1345
|
+
import * as adapter from './external-token-adapter';
|
|
1346
|
+
export interface AuthUser { id: string; loginId: string; name: string; email: string; roles: string[]; }
|
|
1347
|
+
interface Value { user: AuthUser | null; loading: boolean; login: () => Promise<void>; logout: () => Promise<void>; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; }
|
|
1348
|
+
const Context = createContext<Value | null>(null);
|
|
1349
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
1350
|
+
const [user, setUser] = useState<AuthUser | null>(null); const [loading, setLoading] = useState(true);
|
|
1351
|
+
useEffect(() => { authenticatedFetch('/api/auth/me').then(async r => {
|
|
1352
|
+
if (!r.ok || !(r.headers.get('content-type') || '').includes('application/json')) return null;
|
|
1353
|
+
return r.json() as Promise<AuthUser>;
|
|
1354
|
+
}).then(setUser).catch(() => setUser(null)).finally(() => setLoading(false)); }, []);
|
|
1355
|
+
const hasRole = (role: string) => user?.roles.includes(role) ?? false;
|
|
1356
|
+
const logout = async () => { await adapter.logout(); setUser(null); };
|
|
1357
|
+
return <Context.Provider value={{ user, loading, login: adapter.login, logout, hasRole, hasAnyRole: roles => roles.some(hasRole) }}>{children}</Context.Provider>;
|
|
1358
|
+
}
|
|
1359
|
+
export function useAuth() { const value = useContext(Context); if (!value) throw new Error('useAuth must be used within an AuthProvider'); return value; }
|
|
1360
|
+
`;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
export function generateExternalTokenVerifierTS(): string {
|
|
1364
|
+
return `export interface ExternalAuthPrincipal { userId: string; userDetails: string; roles: string[]; }
|
|
1365
|
+
export class ExternalTokenVerificationError extends Error { constructor(public status: 401 | 503, message: string) { super(message); } }
|
|
1366
|
+
/** Implement IdP verification here. Never decode and trust a token without verification. */
|
|
1367
|
+
export async function verifyExternalToken(_token: string): Promise<ExternalAuthPrincipal> {
|
|
1368
|
+
throw new Error('External token verifier is not configured');
|
|
1369
|
+
}
|
|
1370
|
+
`;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
export function generateExternalTokenHelperTS(): string {
|
|
1374
|
+
return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1375
|
+
import { verifyExternalToken, ExternalTokenVerificationError, type ExternalAuthPrincipal } from './external-token-verifier';
|
|
1376
|
+
export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
|
|
1377
|
+
export async function requireAuth(request: HttpRequest): Promise<ExternalAuthPrincipal> {
|
|
1378
|
+
const header = request.headers.get('authorization') || '';
|
|
1379
|
+
if (!header.startsWith('Bearer ') || !header.slice(7)) throw new AuthError(401, 'Missing or invalid Authorization header');
|
|
1380
|
+
try { const principal = await verifyExternalToken(header.slice(7));
|
|
1381
|
+
if (!principal?.userId || !Array.isArray(principal.roles)) throw new AuthError(401, 'Invalid external principal');
|
|
1382
|
+
return principal;
|
|
1383
|
+
} 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'); }
|
|
1384
|
+
}
|
|
1385
|
+
export function requireRoles(user: ExternalAuthPrincipal, roles: string[]) { if (!roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions'); }
|
|
1386
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null; }
|
|
1387
|
+
`;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
export function generateExternalAuthMeTS(): string {
|
|
1391
|
+
return `import { app, HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1392
|
+
import { requireAuth, handleAuthError } from './auth/jwt-helper';
|
|
1393
|
+
app.http('external-auth-me', { methods: ['GET'], route: 'auth/me', authLevel: 'function', handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
|
|
1394
|
+
try { const p = await requireAuth(request); return { status: 200, jsonBody: { id: p.userId, loginId: p.userDetails, name: p.userDetails, email: '', roles: p.roles } }; }
|
|
1395
|
+
catch (error) { return handleAuthError(error) || { status: 500, jsonBody: { error: 'Authentication failed' } }; }
|
|
1396
|
+
} });
|
|
1397
|
+
`;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
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`; }
|
|
1401
|
+
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`; }
|
|
1402
|
+
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`; }
|
|
1403
|
+
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`; }
|
|
1404
|
+
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`; }
|
|
1405
|
+
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`; }
|
|
1406
|
+
export function generateSwaAuthHelperPython(): string {
|
|
1407
|
+
return `import base64
|
|
1408
|
+
import json
|
|
1409
|
+
import azure.functions as func
|
|
1410
|
+
|
|
1411
|
+
class AuthError(Exception):
|
|
1412
|
+
def __init__(self, status, message): self.status, self.message = status, message
|
|
1413
|
+
|
|
1414
|
+
def require_auth(req):
|
|
1415
|
+
encoded = req.headers.get("x-ms-client-principal")
|
|
1416
|
+
if not encoded: raise AuthError(401, "Authentication required")
|
|
1417
|
+
try:
|
|
1418
|
+
value = json.loads(base64.b64decode(encoded).decode("utf-8"))
|
|
1419
|
+
roles = [r for r in value.get("userRoles", []) if isinstance(r, str)]
|
|
1420
|
+
if not value.get("userId") or "authenticated" not in roles: raise ValueError()
|
|
1421
|
+
return {"id": value["userId"], "loginId": value.get("userDetails", ""), "roles": roles}
|
|
1422
|
+
except Exception: raise AuthError(401, "Invalid client principal")
|
|
1423
|
+
|
|
1424
|
+
def require_roles(user, roles):
|
|
1425
|
+
if roles and not any(role in user["roles"] for role in roles): raise AuthError(403, "Insufficient permissions")
|
|
1426
|
+
|
|
1427
|
+
def handle_auth_error(error):
|
|
1428
|
+
return func.HttpResponse(json.dumps({"error": error.message}), status_code=error.status, mimetype="application/json") if isinstance(error, AuthError) else None
|
|
1429
|
+
`;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
export function generateSwaAuthHelperCSharp(): string {
|
|
1433
|
+
return `using System.Text;
|
|
1434
|
+
using System.Text.Json;
|
|
1435
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
1436
|
+
|
|
1437
|
+
namespace Functions.Auth;
|
|
1438
|
+
|
|
1439
|
+
public class JwtPayload
|
|
1440
|
+
{
|
|
1441
|
+
public string Sub { get; set; } = "";
|
|
1442
|
+
public string LoginId { get; set; } = "";
|
|
1443
|
+
public string[] Roles { get; set; } = Array.Empty<string>();
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
public static class JwtHelper
|
|
1447
|
+
{
|
|
1448
|
+
public static async Task<(JwtPayload?, HttpResponseData?)> Authorize(HttpRequestData request, params string[] requiredRoles)
|
|
1449
|
+
{
|
|
1450
|
+
try
|
|
1451
|
+
{
|
|
1452
|
+
var encoded = request.Headers.TryGetValues("x-ms-client-principal", out var values) ? values.FirstOrDefault() : null;
|
|
1453
|
+
if (string.IsNullOrWhiteSpace(encoded)) throw new InvalidOperationException();
|
|
1454
|
+
using var document = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(encoded)));
|
|
1455
|
+
var root = document.RootElement;
|
|
1456
|
+
var userId = root.GetProperty("userId").GetString() ?? "";
|
|
1457
|
+
var details = root.TryGetProperty("userDetails", out var detailValue) ? detailValue.GetString() ?? "" : "";
|
|
1458
|
+
var roles = root.TryGetProperty("userRoles", out var roleValue) && roleValue.ValueKind == JsonValueKind.Array
|
|
1459
|
+
? roleValue.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.String).Select(v => v.GetString()!).ToArray()
|
|
1460
|
+
: Array.Empty<string>();
|
|
1461
|
+
if (string.IsNullOrEmpty(userId) || !roles.Contains("authenticated")) throw new InvalidOperationException();
|
|
1462
|
+
if (requiredRoles.Length > 0 && !requiredRoles.Any(roles.Contains))
|
|
1463
|
+
{
|
|
1464
|
+
var forbidden = request.CreateResponse(System.Net.HttpStatusCode.Forbidden);
|
|
1465
|
+
await forbidden.WriteAsJsonAsync(new { error = "Forbidden" });
|
|
1466
|
+
return (null, forbidden);
|
|
1467
|
+
}
|
|
1468
|
+
return (new JwtPayload { Sub = userId, LoginId = details, Roles = roles }, null);
|
|
1469
|
+
}
|
|
1470
|
+
catch
|
|
1471
|
+
{
|
|
1472
|
+
var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
|
|
1473
|
+
await unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" });
|
|
1474
|
+
return (null, unauthorized);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
`;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1227
1481
|
// ============================================================
|
|
1228
1482
|
// 13. scaffold 用ロールガード挿入ヘルパー
|
|
1229
1483
|
// ============================================================
|
|
@@ -1238,16 +1492,16 @@ export function generateAuthImportTS(): string {
|
|
|
1238
1492
|
/**
|
|
1239
1493
|
* TypeScript Functions のハンドラー先頭に挿入する認証チェックコードを生成
|
|
1240
1494
|
*/
|
|
1241
|
-
export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
|
|
1495
|
+
export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write', provider?: AuthProvider): string {
|
|
1242
1496
|
const roles = operation === 'read'
|
|
1243
1497
|
? (policy.read || policy.roles || [])
|
|
1244
1498
|
: (policy.write || policy.roles || []);
|
|
1245
1499
|
|
|
1246
1500
|
if (roles.length > 0) {
|
|
1247
|
-
return ` const authUser = requireAuth(request);
|
|
1501
|
+
return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);
|
|
1248
1502
|
requireRoles(authUser, ${JSON.stringify(roles)});`;
|
|
1249
1503
|
}
|
|
1250
|
-
return ` const authUser = requireAuth(request);`;
|
|
1504
|
+
return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);`;
|
|
1251
1505
|
}
|
|
1252
1506
|
|
|
1253
1507
|
/**
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ApiModelConnectorConfig,
|
|
12
12
|
ConnectorOperation,
|
|
13
13
|
ModelAuthPolicy,
|
|
14
|
+
AuthProvider,
|
|
14
15
|
} from "../../types";
|
|
15
16
|
import { generateAuthImportTS, generateAuthGuardTS } from "./auth-generator";
|
|
16
17
|
|
|
@@ -24,7 +25,8 @@ export function generateRdbConnectorFunctionTS(
|
|
|
24
25
|
sharedPackageName: string,
|
|
25
26
|
connectorDef: RdbConnectorConfig,
|
|
26
27
|
modelConnector: RdbModelConnectorConfig,
|
|
27
|
-
authPolicy?: ModelAuthPolicy
|
|
28
|
+
authPolicy?: ModelAuthPolicy,
|
|
29
|
+
authProvider?: AuthProvider
|
|
28
30
|
): string {
|
|
29
31
|
const modelCamel = toCamelCase(model.name);
|
|
30
32
|
const schemaName = model.schemaName;
|
|
@@ -123,7 +125,7 @@ export function generateRdbConnectorFunctionTS(
|
|
|
123
125
|
// Auth guard setup
|
|
124
126
|
const hasAuth = !!authPolicy;
|
|
125
127
|
const authImport = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
|
|
126
|
-
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read')}\n` : '';
|
|
128
|
+
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
|
|
127
129
|
const authCatchBlock = hasAuth
|
|
128
130
|
? ` const authErr = handleAuthError(error);
|
|
129
131
|
if (authErr) return authErr;\n `
|
|
@@ -143,7 +145,7 @@ ${getConnection}
|
|
|
143
145
|
app.http('${modelCamel}-get-all', {
|
|
144
146
|
methods: ['GET'],
|
|
145
147
|
route: '${modelCamel}',
|
|
146
|
-
authLevel: '
|
|
148
|
+
authLevel: 'function',
|
|
147
149
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
148
150
|
try {${readGuard}
|
|
149
151
|
${queryAll}
|
|
@@ -162,7 +164,7 @@ app.http('${modelCamel}-get-all', {
|
|
|
162
164
|
app.http('${modelCamel}-get-by-id', {
|
|
163
165
|
methods: ['GET'],
|
|
164
166
|
route: '${modelCamel}/{id}',
|
|
165
|
-
authLevel: '
|
|
167
|
+
authLevel: 'function',
|
|
166
168
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
167
169
|
try {${readGuard}
|
|
168
170
|
const id = request.params.id;
|
|
@@ -190,7 +192,8 @@ export function generateApiConnectorFunctionTS(
|
|
|
190
192
|
sharedPackageName: string,
|
|
191
193
|
connectorDef: ApiConnectorConfig,
|
|
192
194
|
modelConnector: ApiModelConnectorConfig,
|
|
193
|
-
authPolicy?: ModelAuthPolicy
|
|
195
|
+
authPolicy?: ModelAuthPolicy,
|
|
196
|
+
authProvider?: AuthProvider
|
|
194
197
|
): string {
|
|
195
198
|
const modelCamel = toCamelCase(model.name);
|
|
196
199
|
const schemaName = model.schemaName;
|
|
@@ -203,8 +206,8 @@ export function generateApiConnectorFunctionTS(
|
|
|
203
206
|
// Auth guard setup (user access control, separate from connector auth to external APIs)
|
|
204
207
|
const hasAuth = !!authPolicy;
|
|
205
208
|
const authImportLine = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
|
|
206
|
-
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read')}\n` : '';
|
|
207
|
-
const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write')}\n` : '';
|
|
209
|
+
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
|
|
210
|
+
const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write', authProvider)}\n` : '';
|
|
208
211
|
const authCatchBlock = hasAuth
|
|
209
212
|
? ` const authErr = handleAuthError(error);
|
|
210
213
|
if (authErr) return authErr;\n `
|
|
@@ -229,7 +232,7 @@ ${authSetup.helperCode}
|
|
|
229
232
|
app.http('${modelCamel}-get-all', {
|
|
230
233
|
methods: ['GET'],
|
|
231
234
|
route: '${modelCamel}',
|
|
232
|
-
authLevel: '
|
|
235
|
+
authLevel: 'function',
|
|
233
236
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
234
237
|
try {${readGuard}
|
|
235
238
|
const url = getBaseUrl() + '${epPath}';
|
|
@@ -263,7 +266,7 @@ app.http('${modelCamel}-get-all', {
|
|
|
263
266
|
app.http('${modelCamel}-get-by-id', {
|
|
264
267
|
methods: ['GET'],
|
|
265
268
|
route: '${modelCamel}/{id}',
|
|
266
|
-
authLevel: '
|
|
269
|
+
authLevel: 'function',
|
|
267
270
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
268
271
|
try {${readGuard}
|
|
269
272
|
const id = request.params.id;
|
|
@@ -303,7 +306,7 @@ app.http('${modelCamel}-get-by-id', {
|
|
|
303
306
|
app.http('${modelCamel}-create', {
|
|
304
307
|
methods: ['POST'],
|
|
305
308
|
route: '${modelCamel}',
|
|
306
|
-
authLevel: '
|
|
309
|
+
authLevel: 'function',
|
|
307
310
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
308
311
|
try {${writeGuard}
|
|
309
312
|
const body = await request.json();
|
|
@@ -341,7 +344,7 @@ app.http('${modelCamel}-create', {
|
|
|
341
344
|
app.http('${modelCamel}-update', {
|
|
342
345
|
methods: ['PUT'],
|
|
343
346
|
route: '${modelCamel}/{id}',
|
|
344
|
-
authLevel: '
|
|
347
|
+
authLevel: 'function',
|
|
345
348
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
346
349
|
try {${writeGuard}
|
|
347
350
|
const id = request.params.id;
|
|
@@ -381,7 +384,7 @@ app.http('${modelCamel}-update', {
|
|
|
381
384
|
app.http('${modelCamel}-delete', {
|
|
382
385
|
methods: ['DELETE'],
|
|
383
386
|
route: '${modelCamel}/{id}',
|
|
384
|
-
authLevel: '
|
|
387
|
+
authLevel: 'function',
|
|
385
388
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
386
389
|
try {${writeGuard}
|
|
387
390
|
const id = request.params.id;
|
|
@@ -496,7 +499,7 @@ export function generateRdbConnectorFunctionCSharp(
|
|
|
496
499
|
methods += `
|
|
497
500
|
[Function("${modelCamel}GetAll")]
|
|
498
501
|
public async Task<HttpResponseData> GetAll(
|
|
499
|
-
[HttpTrigger(AuthorizationLevel.
|
|
502
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}")] HttpRequestData request)
|
|
500
503
|
{
|
|
501
504
|
try
|
|
502
505
|
{
|
|
@@ -531,7 +534,7 @@ export function generateRdbConnectorFunctionCSharp(
|
|
|
531
534
|
methods += `
|
|
532
535
|
[Function("${modelCamel}GetById")]
|
|
533
536
|
public async Task<HttpResponseData> GetById(
|
|
534
|
-
[HttpTrigger(AuthorizationLevel.
|
|
537
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
|
|
535
538
|
string id)
|
|
536
539
|
{
|
|
537
540
|
try
|
|
@@ -618,7 +621,7 @@ export function generateApiConnectorFunctionCSharp(
|
|
|
618
621
|
methods += `
|
|
619
622
|
[Function("${modelCamel}GetAll")]
|
|
620
623
|
public async Task<HttpResponseData> GetAll(
|
|
621
|
-
[HttpTrigger(AuthorizationLevel.
|
|
624
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}")] HttpRequestData request)
|
|
622
625
|
{
|
|
623
626
|
try
|
|
624
627
|
{
|
|
@@ -651,7 +654,7 @@ export function generateApiConnectorFunctionCSharp(
|
|
|
651
654
|
methods += `
|
|
652
655
|
[Function("${modelCamel}GetById")]
|
|
653
656
|
public async Task<HttpResponseData> GetById(
|
|
654
|
-
[HttpTrigger(AuthorizationLevel.
|
|
657
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
|
|
655
658
|
string id)
|
|
656
659
|
{
|
|
657
660
|
try
|
|
@@ -689,7 +692,7 @@ export function generateApiConnectorFunctionCSharp(
|
|
|
689
692
|
methods += `
|
|
690
693
|
[Function("${modelCamel}Create")]
|
|
691
694
|
public async Task<HttpResponseData> Create(
|
|
692
|
-
[HttpTrigger(AuthorizationLevel.
|
|
695
|
+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "${modelCamel}")] HttpRequestData request)
|
|
693
696
|
{
|
|
694
697
|
try
|
|
695
698
|
{
|
|
@@ -725,7 +728,7 @@ export function generateApiConnectorFunctionCSharp(
|
|
|
725
728
|
methods += `
|
|
726
729
|
[Function("${modelCamel}Update")]
|
|
727
730
|
public async Task<HttpResponseData> Update(
|
|
728
|
-
[HttpTrigger(AuthorizationLevel.
|
|
731
|
+
[HttpTrigger(AuthorizationLevel.Function, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
|
|
729
732
|
string id)
|
|
730
733
|
{
|
|
731
734
|
try
|
|
@@ -759,7 +762,7 @@ export function generateApiConnectorFunctionCSharp(
|
|
|
759
762
|
methods += `
|
|
760
763
|
[Function("${modelCamel}Delete")]
|
|
761
764
|
public async Task<HttpResponseData> Delete(
|
|
762
|
-
[HttpTrigger(AuthorizationLevel.
|
|
765
|
+
[HttpTrigger(AuthorizationLevel.Function, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
|
|
763
766
|
string id)
|
|
764
767
|
{
|
|
765
768
|
try
|