OpenBankProject / OpenBankProject/OBP-API

Consent-JWT / Consent-Id / PSD2-CERT header lookups are case-sensitive, so they silently fail over HTTP/2

Open Beginner friendly
#2,914 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
1.7k
Forks
482
Avg merge
1d 12h
Merged PRs (30d)
15

Description

Title: Consent-JWT / Consent-Id / PSD2-CERT header lookups are case-sensitive, so they silently fail over HTTP/2

Several request-header lookups in APIUtil.scala compare the header's
name field with == against a fixed-case literal, instead of doing a
case-insensitive comparison:

// obp-api/src/main/scala/code/api/util/APIUtil.scala
def getConsentJWT(requestHeaders: List[HTTPParam]): Option[String] = {
  requestHeaders.toSet.filter(_.name == RequestHeader.`Consent-JWT`).toList match {
    case x :: Nil => Some(x.values.mkString(", "))
    case _ => requestHeaders.toSet.filter(_.name == RequestHeader.`Consent-Id`).toList match {
      case x :: Nil => Some(x.values.mkString(", "))
      case _ => None
    }
  }
}

The same pattern appears for Consent-ID (a second, differently-cased
variant of the same header exists a few lines below Consent-Id),
PSD2-CERT, and TPP-Signature-Certificate.

Why this breaks under HTTP/2: RFC 7540 §8.1.2 requires HTTP/2 header
field names to be sent in lowercase on the wire. Any HTTP/2 client — which
is most modern HTTP clients by default when talking to a server that
advertises h2 via ALPN — sends consent-jwt, not Consent-JWT. The
== comparison against the mixed-case literal never matches, so
hasConsentJWT/getConsentJWT return false/None even though the
header is present with the correct name and value. The request then falls
through to whatever the next auth branch is, and — in our case — ends up
with a generic OBP-20001: User not logged in instead of ever reaching
Consent.checkConsent/applyConsentRulesCommon, which made this
confusing to diagnose: none of the consent-specific debug logging in
ConsentUtil.scala ever fired, because that code path was never entered.

Reproduction, against a real instance with consents.allowed=true, a
consent already created via POST /obp/v5.1.0/my/consents/IMPLICIT and
successfully answered via POST /obp/v3.1.0/banks/BANK_ID/consents/CONSENT_ID/challenge
(consent status ACCEPTED, verified independently):

# Over HTTP/2 (curl's and most clients' default for an ALPN-h2 server)
curl "https://<host>/obp/v5.1.0/users/current/user_id" \
  -H "Consent-JWT: <the accepted consent's jwt>" \
  -H "Consumer-Key: <consumer key>"
# -> HTTP 401 {"code":401,"message":"OBP-20001: User not logged in. Authentication is required!"}

# Identical request, forced to HTTP/1.1
curl --http1.1 "https://<host>/obp/v5.1.0/users/current/user_id" \
  -H "Consent-JWT: <the same jwt>" \
  -H "Consumer-Key: <consumer key>"
# -> HTTP 200 {"user_id":"..."}

Same consent, same JWT, same headers — the only difference is the wire
casing HTTP/1.1 vs HTTP/2 puts on the header name, and that alone flips
the result between "not logged in" and a correctly resolved user.

Suggested fix: compare header names case-insensitively, the way
getRequestHeader() a few lines below already does it correctly
(_.name.toLowerCase == name.toLowerCase). Applies to at least
getConsentJWT, getConsentIdRequestHeaderValue, `getPSD2-CERT`,
and the Consent-ID variant — worth a broader grep across APIUtil.scala
for the same filter(_.name == RequestHeader...) pattern to catch any
others.

Environment: reproduced against openbankproject/obp-api:latest,
commit b5a5765a185d173392c9d9c8fe36a4381097d954 (current develop HEAD
as of 2026-09-16), Kubernetes (k3s), behind an ingress-nginx TLS
termination that negotiates HTTP/2 with clients — but the bug is in the
header comparison itself and is not specific to this deployment; any
HTTP/2 client hitting these endpoints on any OBP-API instance should
reproduce it the same way.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in obp-api/src/main/scala/code/api/util/APIUtil.scala, reading getConsentJWT, getConsentIdRequestHeaderValue, getPSD2-CERT, and the nearby header lookups. Search the file for filter(_.name == RequestHeader...) and compare the behavior with getRequestHeader(). Done means lowercase HTTP/2 header names resolve the same consent values as mixed-case HTTP/1.1 requests.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
api, authentication, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.