Skip to content

KNOX-3424: Dynamic audience handling in the KNOXTOKEN service - #1356

Open
hanicz wants to merge 2 commits into
apache:masterfrom
hanicz:KNOX-3424
Open

KNOX-3424: Dynamic audience handling in the KNOXTOKEN service#1356
hanicz wants to merge 2 commits into
apache:masterfrom
hanicz:KNOX-3424

Conversation

@hanicz

@hanicz hanicz commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

KNOX-3424 - Dynamic audience handling in the KNOXTOKEN service

What changes were proposed in this pull request?

Callers can now request a token's aud claim per request via an audience query parameter, validated against the existing knox.token.audiences whitelist to prevent audience/token spoofing.

Behavior

  • No audience param → unchanged; the statically configured knox.token.audiences are used.
  • audience param + no whitelist configured → 400 (secure by default — nothing to validate against).
  • audience param + all values in the whitelist → only the requested audience(s) land in aud.
  • audience param + any value not whitelisted → 400.
  • Multiple audiences allowed (comma-separated and/or repeated params); exact match only, whitespace trimmed.

How was this patch tested?

Unit tests, local tests

curl -sku guest:guest-password -X GET \
  "https://localhost:8443/gateway/tokenissuer/knoxtoken/api/v2/token?lifespan=P0DT1H0M&audience=test1"
{
  "error": "No audiences are configured; cannot honor a requested audience.",
  "code": 100
}
curl -sku guest:guest-password -X GET \
  "https://localhost:8443/gateway/tokenissuer/knoxtoken/api/v2/token?lifespan=P0DT1H0M" \
| jq -r '.access_token' \
| cut -d. -f2 \
| { read p; pad=$(( (4 - ${#p} % 4) % 4 )); printf '%s%s' "$p" "$(printf '%*s' "$pad" '' | tr ' ' '=')" | tr '_-' '/+' | base64 -d; } \
| jq '{aud}'
{
  "aud": null
}

<param>
    <name>knox.token.audiences</name>
    <value>test1,test2</value>
</param>
curl -sku guest:guest-password -X GET \
  "https://localhost:8443/gateway/tokenissuer/knoxtoken/api/v2/token?lifespan=P0DT1H0M" \
| jq -r '.access_token' \
| cut -d. -f2 \
| { read p; pad=$(( (4 - ${#p} % 4) % 4 )); printf '%s%s' "$p" "$(printf '%*s' "$pad" '' | tr ' ' '=')" | tr '_-' '/+' | base64 -d; } \
| jq '{aud}'
{
  "aud": [
    "test1",
    "test2"
  ]
}
curl -sku guest:guest-password -X GET \
  "https://localhost:8443/gateway/tokenissuer/knoxtoken/api/v2/token?lifespan=P0DT1H0M&audience=test1,test2" \
| jq -r '.access_token' \
| cut -d. -f2 \
| { read p; pad=$(( (4 - ${#p} % 4) % 4 )); printf '%s%s' "$p" "$(printf '%*s' "$pad" '' | tr ' ' '=')" | tr '_-' '/+' | base64 -d; } \
| jq '{aud}'
{
  "aud": [
    "test1",
    "test2"
  ]
}

curl -sku guest:guest-password -X GET \
  "https://localhost:8443/gateway/tokenissuer/knoxtoken/api/v2/token?lifespan=P0DT1H0M&audience=test1" \
| jq -r '.access_token' \
| cut -d. -f2 \
| { read p; pad=$(( (4 - ${#p} % 4) % 4 )); printf '%s%s' "$p" "$(printf '%*s' "$pad" '' | tr ' ' '=')" | tr '_-' '/+' | base64 -d; } \
| jq '{aud}'
{
  "aud": "test1"
}
curl -sku guest:guest-password -X GET \
  "https://localhost:8443/gateway/tokenissuer/knoxtoken/api/v2/token?lifespan=P0DT1H0M&audience=test1,bad"
{
  "error": "The requested audience 'bad' is not allowed.",
  "code": 100
}
curl -sku guest:guest-password -X GET \
  "https://localhost:8443/gateway/tokenissuer/knoxtoken/api/v2/token?lifespan=P0DT1H0M&audience=bad"
{
  "error": "The requested audience 'bad' is not allowed.",
  "code": 100
}

Integration Tests

N/A

UI changes

N/A

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Test Results

 4 files   4 suites   10s ⏱️
55 tests 55 ✅ 0 💤 0 ❌
66 runs  66 ✅ 0 💤 0 ❌

Results for commit f0962a3.

♻️ This comment has been updated with latest results.

@hsheinblatt hsheinblatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Tamás. See comment below, but I believe this logic needs to be moved to the filter. We'll need it there for knoxidf, and it would be confusing to have multiple audience request validation paths. We can discuss if that decision should be overruled and we are to put it in the token resource -- I had that plan too initially, but on retrospect, putting it in the filter is more consistent with the filter philosophy.

setupPublicCertPEM();
String jku = getJku();

final List<String> audiences;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense, but for the RFC 8693 extension, we'll have to validate the audience in the filter. We're going to need it for this release of knoxidf, and we'll have to validate it against policy in the filter stage. For same-subject exchanges, there are conventions (it's optional, but vendors use it in somewhat standard ways). Most will try to validate it against some kind of policy, like a stored allowed list in the client_id registration, per subject allow list, or the subject token aud list. But these are the kinds of things we're planning to put in the filter logic. I had initially thought to put it in knoxidf TokenResource that overrides this class, but that path was argued against: though this is kind of a 'token exchange request authorization' step rather than a 'token authorization' step, it was still required to put in the filter.

I had more in mind setting a request parameter for the resolved audience to use that the token resource would read instead of the hardcoded targetAudience -- or that would be replaced by the dynamic value derived in the filter. I hadn't designed this in detail yet as I was recently informed the logic was to be in the filter.

So, for consistency, it would be better to put this logic in the same place. Then there's only one flow that validates the requested audience, and we just branch off that flow based on how we want to authorize it based on some config parameter -- whether to use delegation authz, or same-subject exchange authz, or the whitelist.

for (String audience : requested) {
if (!targetAudiences.contains(audience)) {
throw new AudienceValidationException("The requested audience '" + audience + "' is not allowed.",
ErrorCode.INVALID_AUDIENCE);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all these errors, they'll end up as invalid_request type errors for the RFC 8693 flows. So for consistency with #1354, we'll need to ensure the right error mapping happens in the response. See comment above, but if this logic moves to the filter, then that part will be easier.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants