@@ -1225,6 +1225,24 @@ public static List<string> FindUnknownConfigKeys(JsonNode? defaults, JsonNode? a
12251225 {
12261226 var fullPath = string . IsNullOrEmpty ( path ) ? kvp . Key : $ "{ path } :{ kvp . Key } ";
12271227
1228+ // Auth:Schemes is a name-keyed open dictionary, but the keys *inside* each named scheme
1229+ // are not arbitrary — they depend on the scheme's Type. Pick a type-discriminated schema
1230+ // so any name (including the docs-example names short_session/api_token/admin_jwt) gets
1231+ // the same per-type validation. Without this, a scheme name that happens to match an
1232+ // example in defaults would be validated against that example's incomplete key set,
1233+ // flagging perfectly valid per-type override keys (Bug A in CONFIG_VALIDATION_NAMED_SCHEMES_BUG.md).
1234+ if ( string . Equals ( path , "Auth:Schemes" , StringComparison . Ordinal ) && kvp . Value is JsonObject schemeObj )
1235+ {
1236+ var schemeSchema = GetAuthSchemeSchemaByType ( schemeObj ) ;
1237+ if ( schemeSchema is not null )
1238+ {
1239+ warnings . AddRange ( FindUnknownConfigKeys ( schemeSchema , schemeObj , fullPath ) ) ;
1240+ }
1241+ // No/invalid Type: skip validation. RegisterAuthSchemes throws a clear error at
1242+ // startup for missing/invalid Type; no point double-reporting it here.
1243+ continue ;
1244+ }
1245+
12281246 if ( ContainsKeyIgnoreCase ( defaultObj , kvp . Key , out var matchedKey ) )
12291247 {
12301248 var defaultChild = defaultObj [ matchedKey ! ] ;
@@ -1252,6 +1270,77 @@ public static List<string> FindUnknownConfigKeys(JsonNode? defaults, JsonNode? a
12521270 return warnings ;
12531271 }
12541272
1273+ /// <summary>
1274+ /// Returns the per-type schema for a named entry under <c>Auth:Schemes</c>, keyed off the scheme's
1275+ /// <c>Type</c> field. Returns null when Type is missing/invalid so the caller can skip validation
1276+ /// (RegisterAuthSchemes will throw a clearer error at startup).
1277+ /// </summary>
1278+ private static JsonObject ? GetAuthSchemeSchemaByType ( JsonObject scheme )
1279+ {
1280+ string ? type = null ;
1281+ foreach ( var kvp in scheme )
1282+ {
1283+ if ( string . Equals ( kvp . Key , "Type" , StringComparison . OrdinalIgnoreCase ) )
1284+ {
1285+ if ( kvp . Value is JsonValue val && val . TryGetValue < string > ( out var s ) )
1286+ {
1287+ type = s ;
1288+ }
1289+ break ;
1290+ }
1291+ }
1292+ if ( string . IsNullOrWhiteSpace ( type ) )
1293+ {
1294+ return null ;
1295+ }
1296+ if ( string . Equals ( type , "Cookies" , StringComparison . OrdinalIgnoreCase ) )
1297+ {
1298+ return new JsonObject
1299+ {
1300+ [ "Type" ] = "Cookies" ,
1301+ [ "Enabled" ] = true ,
1302+ [ "CookieValid" ] = "14 days" ,
1303+ [ "CookieName" ] = null ,
1304+ [ "CookiePath" ] = null ,
1305+ [ "CookieDomain" ] = null ,
1306+ [ "CookieMultiSessions" ] = true ,
1307+ [ "CookieHttpOnly" ] = true ,
1308+ [ "CookieSameSite" ] = null ,
1309+ [ "CookieSecure" ] = null
1310+ } ;
1311+ }
1312+ if ( string . Equals ( type , "BearerToken" , StringComparison . OrdinalIgnoreCase ) )
1313+ {
1314+ return new JsonObject
1315+ {
1316+ [ "Type" ] = "BearerToken" ,
1317+ [ "Enabled" ] = true ,
1318+ [ "BearerTokenExpire" ] = "1 hour" ,
1319+ [ "BearerTokenRefreshPath" ] = null
1320+ } ;
1321+ }
1322+ if ( string . Equals ( type , "Jwt" , StringComparison . OrdinalIgnoreCase ) )
1323+ {
1324+ return new JsonObject
1325+ {
1326+ [ "Type" ] = "Jwt" ,
1327+ [ "Enabled" ] = true ,
1328+ [ "JwtSecret" ] = null ,
1329+ [ "JwtIssuer" ] = null ,
1330+ [ "JwtAudience" ] = null ,
1331+ [ "JwtExpire" ] = "60 minutes" ,
1332+ [ "JwtRefreshExpire" ] = "7 days" ,
1333+ [ "JwtClockSkew" ] = "5 minutes" ,
1334+ [ "JwtValidateIssuer" ] = false ,
1335+ [ "JwtValidateAudience" ] = false ,
1336+ [ "JwtValidateLifetime" ] = true ,
1337+ [ "JwtValidateIssuerSigningKey" ] = true ,
1338+ [ "JwtRefreshPath" ] = null
1339+ } ;
1340+ }
1341+ return null ;
1342+ }
1343+
12551344 private static bool ContainsKeyIgnoreCase ( JsonObject obj , string key , out string ? matchedKey )
12561345 {
12571346 foreach ( var kvp in obj )
0 commit comments