data-api-builder icon indicating copy to clipboard operation
data-api-builder copied to clipboard

[Bug]: JWT Authentication issues

Open vs-dsva opened this issue 5 months ago • 3 comments

What happened?

I have configured dab with JWT authentication/authorization:

"host": {
      "mode": "production",
      "authentication": {
        "provider": "Custom",
        "jwt": {
          "audience": "System",
          "issuer": "https://my.authentication.com"
        }
      }
    }

I cannot get the token validated.

Token contains

  roles: ["authenticated"]

it seems similar to: https://github.com/Azure/data-api-builder/discussions/2364

Version

1.5.56

What database are you using?

PostgreSQL

What hosting model are you using?

Custom Docker host

Which API approach are you accessing DAB through?

REST

Relevant log output

Request starting HTTP/1.1 GET http://************************/api/u_da_hrm_u_selskap?$first=10 - - -
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[1]
      Failed to validate the token.
      Microsoft.IdentityModel.Tokens.SecurityTokenInvalidIssuerException: IDX10204: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null or empty.
         at Microsoft.IdentityModel.Tokens.Validators.ValidateIssuerAsync(String issuer, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
         at Microsoft.IdentityModel.Tokens.Validators.ValidateIssuer(String issuer, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
         at Microsoft.IdentityModel.Tokens.InternalValidators.ValidateAfterSignatureFailed(SecurityToken securityToken, Nullable`1 notBefore, Nullable`1 expires, IEnumerable`1 audiences, TokenValidationParameters validationParameters, BaseConfiguration configuration)
         at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
         at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignatureAndIssuerSecurityKey(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
         at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateJWSAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[7]
      Bearer was not authenticated. Failure message: IDX10204: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null or empty.
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HNEU9DJLM514", Request id "0HNEU9DJLM514:00000001": An unhandled exception was thrown by the application.
      System.InvalidOperationException: No authentication handler is registered for the scheme 'OAuthAuthentication'. The registered schemes are: Bearer. Did you forget to call AddAuthentication().Add[SomeAuthHandler]("OAuthAuthentication",...)?
         at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
         at Azure.DataApiBuilder.Core.AuthenticationHelpers.ClientRoleHeaderAuthenticationMiddleware.InvokeAsync(HttpContext httpContext) in /_/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs:line 76
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Azure.DataApiBuilder.Service.Startup.<>c__DisplayClass19_0.<<Configure>b__3>d.MoveNext() in /_/src/Service/Startup.cs:line 538
      --- End of stack trace from previous location ---
         at Azure.DataApiBuilder.Core.Services.PathRewriteMiddleware.InvokeAsync(HttpContext httpContext) in /_/src/Core/Services/PathRewriteMiddleware.cs:line 89
         at Azure.DataApiBuilder.Core.Services.CorrelationIdMiddleware.Invoke(HttpContext httpContext) in /_/src/Core/Services/CorrelationIdMiddleware.cs:line 53
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/1.1 GET http://*****************/api/u_da_hrm_u_selskap?$first=10 - 500 0 - 16.5796ms

Code of Conduct

  • [x] I agree to follow this project's Code of Conduct

vs-dsva avatar Aug 18 '25 12:08 vs-dsva

@JerryNixon: Here is my output. I will have to redact some output:

Call:

curl -v -H "Authorization: Bearer eyJraWQiOi...Yw" https://dab-***/api/u_da_hrm_u_histhjem
* Host dab-***:443 was resolved.
* IPv6: (none)
* IPv4: 34.***.***.***, 34.***.***.***
*   Trying 34.***.***.***:443...
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* Connected to dab-*** (34.***.***.***) port 443
* using HTTP/1.x
> GET /api/u_da_hrm_u_histhjem HTTP/1.1
> Host: dab-***
> User-Agent: curl/8.14.1
> Accept: */*
> Authorization: Bearer eyJraWQiOi...Yw
>
* Request completely sent off
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
< HTTP/1.1 500 Internal Server Error
< Content-Length: 0
< Date: Fri, 22 Aug 2025 12:58:33 GMT
< Server: Kestrel
<
* Connection #0 to host dab-*** left intact

Token:

{
  "kid": "7ad2fc5e-6a63-4754-a6d3-2ec6585e908a",
  "alg": "RS256"
}.{
  "aud": "System",
  "sub": "dab",
  "scope": "GETmodule:read",
  "roles": [
    "authenticated"
  ],
  "iss": "https://auth-***",
  "exp": 1755867668,
  "iat": 1755866768,
  "customer": "demo"
}.{
  "e": "AQAB",
  "kty": "RSA",
  "n": "4iL0VTnvqbns-bWtyw4tUFWOK5sOndg_dYFRkkicGeSweKbMV2UcnRsMAxgmWmJjAjS2TWR0bFIGc3mVnXRQ4hhZkMOeb4vW0C7GIrZ5kjgEEIkvYDaqNr-x5H2oJQcYE2UWy-AZ0R-UxVQQt_Vbb-GWqntLJBI8QKlA6caKOmqJ5c3ynGqrWDT6-pLARyUrZhDcMK2J445COzKQOsNBJFfUb8fwbkHY5svgac3Y0mdSdXjDWD6eej3X4AuNBBBCVjlF_gHuTOM6UW47Bg5WZny_xo3LbYt58TyCMGnsuwnSE7foLnDSxc220j-cdwDAUSVivKYADN5w02a3dI3ljw"
}

OpenID Configuration retrieved from https://auth-***/.well-known/openid-configuration:

{
  "issuer": "https://auth-***",
  "authorization_endpoint": "https://auth-***/authz/api/authorization",
  "userinfo_endpoint": "https://auth-***/authz/api/user_info",
  "jwks_uri": "https://auth-***/.well-known/openid-configuration/jwks",
  "response_types_supported": [
    "token"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "grant_types_supported": [
    "client_credentials",
    "refresh_token"
  ]
}

JWKS Configuration is:

{
  "keys": [
    {
      "kty": "RSA",
      "e": "AQAB",
      "kid": "7ad2fc5e-6a63-4754-a6d3-2ec6585e908a",
      "n": "4iL0VTnvqbns-bWtyw4tUFWOK5sOndg_dYFRkkicGeSweKbMV2UcnRsMAxgmWmJjAjS2TWR0bFIGc3mVnXRQ4hhZkMOeb4vW0C7GIrZ5kjgEEIkvYDaqNr-x5H2oJQcYE2UWy-AZ0R-UxVQQt_Vbb-GWqntLJBI8QKlA6caKOmqJ5c3ynGqrWDT6-pLARyUrZhDcMK2J445COzKQOsNBJFfUb8fwbkHY5svgac3Y0mdSdXjDWD6eej3X4AuNBBBCVjlF_gHuTOM6UW47Bg5WZny_xo3LbYt58TyCMGnsuwnSE7foLnDSxc220j-cdwDAUSVivKYADN5w02a3dI3ljw"
    }
  ]
}

Dab config:

{
  "data-source": {
    "database-type": "postgresql",
    "connection-string": "Host=@env('DB_HOST');Port=5432;Username=@env('DB_USER');Password=@env('DB_PASSWORD');Database=@env('DB_USER');Timeout=60;Command Timeout=0;"
  },
  "runtime": {
    "rest": {
      "enabled": true
    },
    "graphql": {
      "enabled": true
    },
    "host": {
      "mode": "production",
      "authentication": {
        "provider": "Custom",
        "jwt": {
          "audience": "System",
          "issuer": "https://dab-***"
        }
      }
    }
  },
  "entities": {
    "u_da_invoicing_u_faktdok": {
      "source": {
        "object": "u_da.invoicing_u_faktdok",
        "type": "view",
        "key-fields": [
          "u_pk"
        ]
      },
      "rest": {
        "methods": [
          "GET"
        ]
      },
      "permissions": [
        {
          "role": "authenticated",
          "actions": [
            "read"
          ]
        },
        {
          "role": "anonymous",
          "actions": [
            "read"
          ]
        }
      ]
    },
    ...
  }
}

Dab logs:

Information: Microsoft.DataApiBuilder 1.5.56
Information: User provided config file: /opt/dab-config.json
Loading config file from /opt/dab-config.json.
Information: Loaded config file: /opt/dab-config.json
Information: Setting default minimum LogLevel: Error for Production mode.
Starting the runtime engine...
Loading config file from /opt/dab-config.json.
Monitoring config: /opt/dab-config.json for hot-reloading.
warn: Microsoft.AspNetCore.DataProtection.Repositories.EphemeralXmlRepository[50]
      Using an in-memory repository. Keys will not be persisted to storage.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[59]
      Neither user profile nor HKLM registry available. Using an ephemeral key repository. Protected data will be unavailable when application exits.
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[58]
      Creating key {d01bca85-3230-4605-b8fc-88abc595b103} with creation date 2025-08-22 12:37:10Z, activation date 2025-08-22 12:37:10Z, and expiration date 2025-11-20 12:37:10Z.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {d01bca85-3230-4605-b8fc-88abc595b103} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /opt
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://dab-***/api - - -
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HNF1F427DFP1", Request id "0HNF1F427DFP1:00000001": An unhandled exception was thrown by the application.
      System.InvalidOperationException: No authentication handler is registered for the scheme 'OAuthAuthentication'. The registered schemes are: Bearer. Did you forget to call AddAuthentication().Add[SomeAuthHandler]("OAuthAuthentication",...)?
         at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
         at Azure.DataApiBuilder.Core.AuthenticationHelpers.ClientRoleHeaderAuthenticationMiddleware.InvokeAsync(HttpContext httpContext) in /_/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs:line 76
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Azure.DataApiBuilder.Service.Startup.<>c__DisplayClass19_0.<<Configure>b__3>d.MoveNext() in /_/src/Service/Startup.cs:line 538
      --- End of stack trace from previous location ---
         at Azure.DataApiBuilder.Core.Services.PathRewriteMiddleware.InvokeAsync(HttpContext httpContext) in /_/src/Core/Services/PathRewriteMiddleware.cs:line 89
         at Azure.DataApiBuilder.Core.Services.CorrelationIdMiddleware.Invoke(HttpContext httpContext) in /_/src/Core/Services/CorrelationIdMiddleware.cs:line 53
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/1.1 GET http://dab-***/api - 500 0 - 60.2469ms

Config generation script (run in the entrypoint of the Docker container):

import psycopg2
import json
import os
import re
from collections import defaultdict

# Get database connection details from environment variables
DB_CONFIG = {
    "dbname": os.getenv("DB_NAME", "postgres"),
    "user": os.getenv("DB_USER", "postgres"),
    "password": os.getenv("DB_PASSWORD", "postgres"),
    "host": os.getenv("DB_HOST", "localhost"),
    "port": int(os.getenv("DB_PORT", 5432))
}

# Schemas to expose (comma-separated environment variable)
SCHEMAS = [_.strip() for _ in os.getenv("DB_SCHEMAS", "public").split(",")]
EXCLUDED = [_.strip() for _ in os.getenv("DB_EXCLUDED", "").split(",")]
MODE = os.getenv("MODE", "production")

JWT_ISSUER = f"{os.getenv('TENANT_FQDN', 'https://localhost')}"
LOG_LEVEL = os.getenv("LOG_LEVEL", "info").lower()

postgresql_to_dab = {
    # Boolean
    "boolean": "bool",
    
    # Number - Integers
    "smallint": "number",
    "integer": "number",
    "bigint": "number",
    
    # Number - Decimals
    "decimal": "number",
    "numeric": "number",
    "real": "number",
    "double precision": "number",
    "money": "number",
    
    # String
    "text": "string",
    "character varying": "string",
    "varchar": "string",
    "character": "string",
    "char": "string",
    "citext": "string",
    
    # Other types considered as String
    "uuid": "string",
    "json": "string",
    "jsonb": "string",
    "xml": "string",
}
	
# Connect to PostgreSQL
conn = psycopg2.connect(**DB_CONFIG)
objs = defaultdict(list)

# Query all tables/views from the selected schemas
for schema in SCHEMAS:
    cur = conn.cursor()
    cur.execute(f"""
    SELECT 
        table_name AS object_name, 
        'TABLE' AS object_type, 
        '' AS parameters
    FROM information_schema.tables
    WHERE table_schema = '{schema}'
    AND table_type = 'BASE TABLE'

    UNION

    SELECT 
        table_name AS object_name, 
        'VIEW' AS object_type, 
        '' AS parameters
    FROM information_schema.views
    WHERE table_schema = '{schema}'

    UNION

    SELECT
        p.proname AS object_name,
        'FUNCTION' AS object_type,
	    pg_get_function_arguments(p.oid) as parameters
    FROM 
        pg_catalog.pg_namespace n
        JOIN pg_catalog.pg_proc p ON p.pronamespace = n.oid
    WHERE   
        p.prokind = 'p'
    AND
        n.nspname = '{schema}';
    """)
    for t in cur.fetchall():
        if t[1] == 'FUNCTION':
            _params = [re.split(r"\s+", x.strip()) for x in t[2].split(',')]
            params = {}
            for p in _params:
                print(p)
                params[p[1]]=postgresql_to_dab.get(p[2], "string")
            objs[schema].append({'name':t[0], 'type':t[1], 'parameters': params})
        else:
            objs[schema].append({'name':t[0], 'type':t[1]})
    cur.close()

   # jobid integer, jobtype character varying, response json
    

print("🔍 Found tables/views:", objs)
conn.close()

# Build DAB config
dab_config = {
    "data-source": {
        "database-type": "postgresql",
        "connection-string": "Host=@env('DB_HOST');Port=5432;Username=@env('DB_USER');Password=@env('DB_PASSWORD');Database=@env('DB_USER');Timeout=60;Command Timeout=0;"
    },
    "runtime": {
        "rest": {
            "enabled": True
        },
        "graphql": {
            "enabled": True
        },
        "host": {
            "mode": MODE,
            "authentication": {
                "provider": "Custom", 
                "jwt": {
                    "audience": "System",
                    "issuer": JWT_ISSUER
                }
            }
        }
    },
    "entities": {},
  }

if MODE == 'development':
	dab_config["runtime"]["host"]["authentication"] = {
		"provider": "Simulator"
	}

for schema in objs:
    for obj in objs[schema]:
        print(f"📦 Adding entity: {schema}.{obj['name']}")
        entity_name = f"{schema}_{obj['name']}"
        if entity_name in EXCLUDED or obj['name'] == "flyway_schema_history" or obj['name'] == "u_migration_marker":
            print(f"Skipping excluded entity: {schema}.{obj['name']}")
            continue
        entity_type = obj['type']
        if entity_type == 'VIEW':
            dab_config["entities"][entity_name] = {
                "source": {
                    "object": f"{schema}.{obj['name']}",
                    "type": "view",
                    "key-fields": ["u_pk"]
                },
                "rest": {
                    "methods": ["GET"]
                },
                "permissions": [
                    {
                        "role": "authenticated",
                        "actions": ["read"]
                    },           
                    {
                        "role": "anonymous",
                        "actions": ["read"]
                    }
                ]
            }
        if entity_type == 'TABLE':
            dab_config["entities"][entity_name] = {
                "source": {
                    "object": f"{schema}.{obj['name']}",
                    "type": "view",
                    "key-fields": ["u_pk"]
                },
                "rest": {
                    "methods": ["GET","POST", "PUT", "DELETE"]
                },
                "permissions": [
                    {
                        "role": "authenticated",
                        "actions": ["read", "create", "update", "delete"]
                    },           
                    {
                        "role": "anonymous",
                        "actions": ["read"]
                    }
                ]
            }
        # else:
        #     dab_config["entities"][entity_name] = {
        #         "source": {
        #             "object": f"{schema}.{obj['name']}",
        #             "type": "stored-procedure",
        #             "parameters": obj['parameters']
        #         },
        #         "rest": {
        #             "methods": ["POST"]  # Functions usually require POST for input
        #         },
        #         "permissions": [
        #             {"role": "authenticated", "actions": ["execute"]}
        #         ]
        #     }

# Save to file
with open("dab-config.json", "w") as f:
    json.dump(dab_config, f, indent=2)

print("✅ Generated dab-config.json with all tables/views from schemas:", SCHEMAS)

vs-dsva avatar Aug 22 '25 13:08 vs-dsva

@JerryNixon: I have checked that the token is correctly forwarded in the request by placing httpbun instead the actual container and i can see that I am getting all headers correctly:

{
  "method": "GET",
  "args": {},
  "headers": {
    "Accept": "application/json, text/plain, */*",
    "Accept-Encoding": "gzip, compress, deflate, br",
    "Authorization": "Bearer eyJraWQiOi...fMG_g",
    "Host": "dab-***",
    "Request-Start-Time": "1755875420645",
    "User-Agent": "bruno-runtime/2.9.1",
    "X-Forwarded-Access-Token": "eyJraWQiOiI...fMG_g",
    "X-Forwarded-Email": "me",
    "X-Forwarded-For": "172.16.191.65, 172.16.174.174",
    "X-Forwarded-Host": "dab-***",
    "X-Forwarded-Port": "80",
    "X-Forwarded-Proto": "http",
    "X-Forwarded-Server": "traefik-66d7b78c46-shzx7",
    "X-Forwarded-User": "me",
    "X-Real-Ip": "172.16.191.65"
  },
  "origin": "127.0.0.1",
  "url": "http://dab-***/get",
  "form": {},
  "data": "",
  "json": null,
  "files": {}
}

vs-dsva avatar Aug 22 '25 15:08 vs-dsva

@JerryNixon @vs-dsva is there any update on this issue? I also cannot get the Custom provider. I'm using v1.5.56 and getting the No authentication handler is registered for the scheme 'OAuthAuthentication' error.

dab-config:
    "host": {
      "authentication": {
        "provider": "Custom",
        "jwt": {
          "audience": "project_id",
          "issuer": "https://api.descope.com/project_id",
          "jwks-uri": "https://api.descope.com/project_id/.well-known/jwks.json"
        }
      },
      "mode": "development"
    },
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HNFNJI639R64", Request id "0HNFNJI639R64:00000001": An unhandled exception was thrown by the application.
      System.InvalidOperationException: No authentication handler is registered for the scheme 'OAuthAuthentication'. The registered schemes are: StaticWebAppsAuthentication, Bearer, SimulatorAuthentication. Did you forget to call AddAuthentication().Add[SomeAuthHandler]("OAuthAuthentication",...)?
         at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
         at Azure.DataApiBuilder.Core.AuthenticationHelpers.ClientRoleHeaderAuthenticationMiddleware.InvokeAsync(HttpContext httpContext) in /_/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs:line 76
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Azure.DataApiBuilder.Service.Startup.<>c__DisplayClass19_0.<<Configure>b__3>d.MoveNext() in /_/src/Service/Startup.cs:line 538
      --- End of stack trace from previous location ---
         at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
         at Azure.DataApiBuilder.Core.Services.PathRewriteMiddleware.InvokeAsync(HttpContext httpContext) in /_/src/Core/Services/PathRewriteMiddleware.cs:line 89
         at Azure.DataApiBuilder.Core.Services.CorrelationIdMiddleware.Invoke(HttpContext httpContext) in /_/src/Core/Services/CorrelationIdMiddleware.cs:line 53
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

mharne avatar Sep 19 '25 17:09 mharne