@@ -39,7 +39,8 @@ public enum CrudApiActionType
3939public enum CrudApiAuthType
4040{
4141 None ,
42- Entra
42+ Entra ,
43+ ApiKey
4344}
4445
4546public sealed class CrudApiEntraAuth
@@ -52,6 +53,13 @@ public sealed class CrudApiEntraAuth
5253 public bool ValidateSigningKey { get ; set ; }
5354}
5455
56+ public sealed class CrudApiApiKeyAuth
57+ {
58+ public string ApiKey { get ; set ; } = string . Empty ;
59+ public string ? HeaderName { get ; set ; }
60+ public string ? QueryParameterName { get ; set ; }
61+ }
62+
5563public sealed class CrudApiAction
5664{
5765 [ System . Text . Json . Serialization . JsonConverter ( typeof ( JsonStringEnumConverter ) ) ]
@@ -67,6 +75,7 @@ public sealed class CrudApiAction
6775public sealed class CrudApiConfiguration
6876{
6977 public IEnumerable < CrudApiAction > Actions { get ; set ; } = [ ] ;
78+ public CrudApiApiKeyAuth ? ApiKeyAuthConfig { get ; set ; }
7079 public string ApiFile { get ; set ; } = "api.json" ;
7180 [ System . Text . Json . Serialization . JsonConverter ( typeof ( JsonStringEnumConverter ) ) ]
7281 public CrudApiAuthType Auth { get ; set ; } = CrudApiAuthType . None ;
@@ -115,6 +124,21 @@ public override async Task InitializeAsync(InitArgs e, CancellationToken cancell
115124 Configuration . Auth = CrudApiAuthType . None ;
116125 }
117126
127+ if ( Configuration . Auth == CrudApiAuthType . ApiKey &&
128+ Configuration . ApiKeyAuthConfig is null )
129+ {
130+ Logger . LogError ( "API Key auth is enabled but no configuration is provided. API will work anonymously." ) ;
131+ Configuration . Auth = CrudApiAuthType . None ;
132+ }
133+
134+ if ( Configuration . Auth == CrudApiAuthType . ApiKey &&
135+ Configuration . ApiKeyAuthConfig is not null &&
136+ string . IsNullOrEmpty ( Configuration . ApiKeyAuthConfig . ApiKey ) )
137+ {
138+ Logger . LogError ( "API Key auth is enabled but no API key is configured. API will work anonymously." ) ;
139+ Configuration . Auth = CrudApiAuthType . None ;
140+ }
141+
118142 if ( ! ProxyUtils . MatchesUrlToWatch ( UrlsToWatch , Configuration . BaseUrl , true ) )
119143 {
120144 Logger . LogWarning (
@@ -221,6 +245,7 @@ private async Task SetupOpenIdConnectConfigurationAsync()
221245 return $ "(?<{ paramName } >[^/&]+)";
222246 } ) ;
223247
248+ var requestUrlWithoutQuery = request . RequestUri . GetLeftPart ( UriPartial . Path ) ;
224249 var parameters = new Dictionary < string , string > ( ) ;
225250 var action = Configuration . Actions . FirstOrDefault ( action =>
226251 {
@@ -231,7 +256,7 @@ private async Task SetupOpenIdConnectConfigurationAsync()
231256
232257 var absoluteActionUrl = ( Configuration . BaseUrl + action . Url ) . Replace ( "//" , "/" , 8 ) ;
233258
234- if ( absoluteActionUrl == request . Url )
259+ if ( absoluteActionUrl == requestUrlWithoutQuery )
235260 {
236261 return true ;
237262 }
@@ -245,7 +270,7 @@ private async Task SetupOpenIdConnectConfigurationAsync()
245270
246271 // convert parameters into named regex groups
247272 var urlRegex = Regex . Replace ( Regex . Escape ( absoluteActionUrl ) . Replace ( "\\ {" , "{" , StringComparison . OrdinalIgnoreCase ) , "({[^}]+})" , parameterMatchEvaluator ) ;
248- var match = Regex . Match ( request . Url , urlRegex ) ;
273+ var match = Regex . Match ( requestUrlWithoutQuery , urlRegex ) ;
249274 if ( ! match . Success )
250275 {
251276 return false ;
@@ -290,12 +315,25 @@ private void AddCORSHeaders(Request request, List<HttpHeader> headers)
290315
291316 headers . Add ( new HttpHeader ( "access-control-allow-origin" , origin ) ) ;
292317
318+ var allowHeaders = new List < string > { "content-type" } ;
319+
293320 if ( Configuration . EntraAuthConfig is not null ||
294321 Configuration . Actions . Any ( a => a . Auth == CrudApiAuthType . Entra ) )
295322 {
296- headers . Add ( new HttpHeader ( "access-control-allow-headers" , " authorization, content-type" ) ) ;
323+ allowHeaders . Add ( " authorization" ) ;
297324 }
298325
326+ if ( Configuration . ApiKeyAuthConfig is not null &&
327+ ! string . IsNullOrEmpty ( Configuration . ApiKeyAuthConfig . HeaderName ) )
328+ {
329+ if ( ! allowHeaders . Contains ( Configuration . ApiKeyAuthConfig . HeaderName , StringComparer . OrdinalIgnoreCase ) )
330+ {
331+ allowHeaders . Add ( Configuration . ApiKeyAuthConfig . HeaderName ) ;
332+ }
333+ }
334+
335+ headers . Add ( new HttpHeader ( "access-control-allow-headers" , string . Join ( ", " , allowHeaders ) ) ) ;
336+
299337 var methods = string . Join ( ", " , Configuration . Actions
300338 . Where ( a => a . Method is not null )
301339 . Select ( a => a . Method )
@@ -307,7 +345,6 @@ private void AddCORSHeaders(Request request, List<HttpHeader> headers)
307345 private bool AuthorizeRequest ( ProxyRequestArgs e , CrudApiAction ? action = null )
308346 {
309347 var authType = action is null ? Configuration . Auth : action . Auth ;
310- var authConfig = action is null ? Configuration . EntraAuthConfig : action . EntraAuthConfig ;
311348
312349 if ( authType == CrudApiAuthType . None )
313350 {
@@ -318,6 +355,56 @@ private bool AuthorizeRequest(ProxyRequestArgs e, CrudApiAction? action = null)
318355 return true ;
319356 }
320357
358+ if ( authType == CrudApiAuthType . ApiKey )
359+ {
360+ return AuthorizeApiKeyRequest ( e ) ;
361+ }
362+
363+ return AuthorizeEntraRequest ( e , action ) ;
364+ }
365+
366+ private bool AuthorizeApiKeyRequest ( ProxyRequestArgs e )
367+ {
368+ var apiKeyAuthConfig = Configuration . ApiKeyAuthConfig ;
369+
370+ Debug . Assert ( apiKeyAuthConfig is not null , "ApiKeyAuthConfig is null when API key auth is required." ) ;
371+
372+ // Check header
373+ if ( ! string . IsNullOrEmpty ( apiKeyAuthConfig . HeaderName ) )
374+ {
375+ var headerValue = e . Session . HttpClient . Request . Headers
376+ . FirstOrDefault ( h => h . Name . Equals ( apiKeyAuthConfig . HeaderName , StringComparison . OrdinalIgnoreCase ) ) ? . Value ;
377+
378+ if ( ! string . IsNullOrEmpty ( headerValue ) && headerValue == apiKeyAuthConfig . ApiKey )
379+ {
380+ return true ;
381+ }
382+ }
383+
384+ // Check query parameter
385+ if ( ! string . IsNullOrEmpty ( apiKeyAuthConfig . QueryParameterName ) )
386+ {
387+ var requestUrl = e . Session . HttpClient . Request . RequestUri ;
388+ var queryString = requestUrl . Query ;
389+ if ( ! string . IsNullOrEmpty ( queryString ) )
390+ {
391+ var queryParams = System . Web . HttpUtility . ParseQueryString ( queryString ) ;
392+ var queryValue = queryParams [ apiKeyAuthConfig . QueryParameterName ] ;
393+ if ( ! string . IsNullOrEmpty ( queryValue ) && queryValue == apiKeyAuthConfig . ApiKey )
394+ {
395+ return true ;
396+ }
397+ }
398+ }
399+
400+ Logger . LogRequest ( "401 Unauthorized. The specified API key is not valid." , MessageType . Failed , new LoggingContext ( e . Session ) ) ;
401+ return false ;
402+ }
403+
404+ private bool AuthorizeEntraRequest ( ProxyRequestArgs e , CrudApiAction ? action = null )
405+ {
406+ var authConfig = action is null ? Configuration . EntraAuthConfig : action . EntraAuthConfig ;
407+
321408 Debug . Assert ( authConfig is not null , "EntraAuthConfig is null when auth is required." ) ;
322409
323410 var token = e . Session . HttpClient . Request . Headers . FirstOrDefault ( h => h . Name . Equals ( "Authorization" , StringComparison . OrdinalIgnoreCase ) ) ? . Value ;
0 commit comments