Skip to content

help request: how to debug weird user info returned/altered in the OpenID connect plugin? #13440

Closed

Description

@ronnybremer

Description

I am using OpenID Connect to protect most of the resources. All is working fine except one intriguing thing.

Upon authenticating APISIX is queuing the IDPs user info endpoint and I have configured the response rewrite plugin to return that as a header:

      response-rewrite:
        headers:
          X-Userinfo: $http_x_userinfo

Works, a base64 encoded user info arrives at the client. However, something has changed inside. The original response from the user info endpoint was (according to their support):

{"sub": "a UID", "name": "Testuser One", "roles": []}

but after base64 decoding the data from the header on the client I see this:

{"sub": "a UID", "name": "Testuser One", "roles": {}}

Note the empty array roles has changed to an object.

In order to debug this I would need to follow the entire chain of moving parts, starting with the OpenID connect plugin. Is there any way to print out to the log the data received from the user info endpoint?

Environment

  • APISIX version (run apisix version): 3.16.0
  • Operating system (run uname -a): official docker image
  • OpenResty / Nginx version (run openresty -V or nginx -V): 1.27.1.2
  • etcd version, if relevant (run curl http://127.0.0.1:9090/v1/server_info):
  • APISIX Dashboard version, if relevant:
  • Plugin runner version, for issues related to plugin runners:
  • LuaRocks version, for installation issues (run luarocks --version):

Activity

  1. Baoyuantop commented on May 27, 2026

    @Baoyuantop
    Contributor

    From the code path, response-rewrite should not be the place where the JSON structure is changed. It only resolves $http_x_userinfo and copies that request header value to the response header.

    The X-Userinfo header is set earlier by the openid-connect plugin. The plugin gets userinfo / response.user from lua-resty-openidc, JSON-encodes it, base64-encodes it, and then sets it as the X-Userinfo request header. To verify what APISIX receives directly from the IDP userinfo endpoint, please temporarily set the APISIX error log level to debug in a test environment, reproduce the request once, and check error.log for the userinfo response: ... line. That log is emitted after lua-resty-openidc calls the userinfo endpoint and before APISIX re-encodes the value into X-Userinfo.

    Please be careful because userinfo may contain sensitive user data. I would not recommend enabling debug logging for a long time in production, and please redact sensitive fields before sharing logs.

    Could you please provide the following details?

    1. The full openid-connect plugin configuration, with secrets/tokens redacted.
    2. Whether this request uses authorization code flow, bearer token introspection, or JWKS validation.
    3. The userinfo response: ... line from debug logs.
    4. The base64-decoded X-Userinfo value for the same request.

    If the debug log already shows roles as {}, APISIX is receiving it in that form from the IDP. If the debug log shows roles as [] but the decoded X-Userinfo shows {}, then we can continue investigating JSON decode/encode preservation in APISIX / lua-resty-openidc.

  2. ronnybremer commented on May 27, 2026

    @ronnybremer
    ContributorAuthor

    Done that, here seems to be the reply received from the user_info endpoint:

    2026/05/27 13:49:44 [debug] 57#57: *27775 [lua] openidc.lua:636: call_userinfo_endpoint(): userinfo response: {"sub":"df538330-2b18-4d70-b5c2-c38bdb6e330c","memberGUID":"df538330-2b18-4d70-b5c2-c38bdb6e330c","name":"Testuser One","role":"DefaultUser","roles":[]}
    

    So the roles is still an array at that point. However, it arrives at the web browser as:

    {"sub":"df538330-2b18-4d70-b5c2-c38bdb6e330c","memberGUID":"df538330-2b18-4d70-b5c2-c38bdb6e330c","name":"Testuser One","roles":{},"role":"DefaultUser"}
    

    My environment:
    Running APISIX as an ingress controller in a Kubernetes cluster.

    OpenID Connect plugin config:

      plugins:
      - name: openid-connect
        enable: true
        config:
          client_id: *redacted*
          client_secret: *redacted*
          scope: "openid offline_access"
          use_nonce: true
          use_pkce: true
          use_jwks: true
          use_jwks: true
          timeout: 10
          discovery: https://idp/.well-known/openid-configuration
          token_endpoint_auth_method: client_secret_post
          redirect_uri: https://portal/.apisix/redirect
          logout_path: /.apisix/logout
          post_logout_redirect_uri: https://idp/logout
          introspection_endpoint: https://idp/token_info
          bearer_only: false
          session.cookie.lifetime: 600
          set_access_token_header: true
          access_token_in_authorization_header: true
          realm: SAD-S
          session:
            secret: *redacted*
          unauth_action: auth
      - name: response-rewrite
        enable: true
        config:
          headers:
            X-Userinfo: $http_x_userinfo

    The route is in front of the portal web server, the IDP is configured for authorization code workflow. Token will be stored in the session cookie (I know it makes the cookie large, so I might change that to Redis in the future).

    Thank you for your help!

  3. added
    bugSomething isn't working
    and removed
    questionlabel for questions asked by users
    on May 29, 2026
  4. shreemaan-abhishek commented on Jun 2, 2026

    @shreemaan-abhishek
    Contributor

    Thanks for the debug log @ronnybremer, that pinned it down. Confirmed: this is a real bug. It is not in response-rewrite or in the plugin logic. The array gets lost during the session cookie round trip.

    How the bug occurs, step by step

    1. Your IDP returns "roles": [].
    2. lua-resty-openidc decodes this JSON into a Lua table. APISIX configures the JSON library to put a special "this is an array" tag on decoded arrays (core/json.lua:35). So at this point roles is an empty table tagged as an array. This is why your debug log still shows [].
    3. The user object is saved into your session cookie. The tag survives, the cookie still contains "roles":[].
    4. On the next request, lua-resty-session decodes the cookie back into a Lua table. Problem: it uses its own private copy of the JSON library (utils.lua:248), and that copy does not apply the array tag. So roles comes back as a plain empty table.
    5. Here is the core issue: in Lua, an empty array and an empty object are the exact same thing, an empty table {}. The only way to tell them apart is that tag, and step 4 just dropped it.
    6. The plugin now encodes this untagged empty table into the X-Userinfo header (openid-connect.lua:804). The JSON encoder has to guess, and its default for an empty table is object. So you get "roles":{}.

    Impact

    • Only empty arrays break. Non-empty arrays like ["admin"] have contents that make them recognizable as arrays, so they are fine.
    • Happens with any session storage (cookie, Redis, shm), they all use the same serializer.
    • X-ID-Token claims are affected the same way.

    Fix and workarounds

    The proper fix is in lua-resty-session: its JSON decoder should tag arrays too. We will raise this upstream and bump the dependency once fixed.

    Until then:

    • Treat {} and [] both as "empty" in your client/upstream code.
    • Or configure the IDP to omit empty array claims.
  5. ronnybremer commented on Jun 2, 2026

    @ronnybremer
    ContributorAuthor

    Thank you @shreemaan-abhishek for the detailed analysis and for finding the bug :) The IDP is outside of my control, but I will configure Rust serde in the meantime to handle both an array and an object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingplugin

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions