KNOX-3424: Dynamic audience handling in the KNOXTOKEN service - #1356
KNOX-3424: Dynamic audience handling in the KNOXTOKEN service#1356hanicz wants to merge 4 commits into
Conversation
Test Results 4 files 4 suites 10s ⏱️ Results for commit 01531a3. ♻️ This comment has been updated with latest results. |
hsheinblatt
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
hsheinblatt
left a comment
There was a problem hiding this comment.
Thanks Tamás. See more detailed comments below, but I'd switch from using a query parameter for the requested audience to a header. A header is generally more secure and how we normally pass values parsed in the filter to the delegated service. If we stick with a query parameter, then when we add RFC 8693 token exchange audience requests, we'll need separate logic for that, and we'll need a separate switch in knoxtoken to disable reading the request from the query string.
| protected static final String TOKEN_TTL_PARAM = TOKEN_PARAM_PREFIX + "ttl"; | ||
| public static final String TOKEN_TYPE_PARAM = TOKEN_PARAM_PREFIX + "type"; | ||
| private static final String TOKEN_AUDIENCES_PARAM = TOKEN_PARAM_PREFIX + "audiences"; | ||
| static final String AUDIENCE_QUERY_PARAM = "audience"; |
There was a problem hiding this comment.
For RFC 8693 token exchange, the audience requested is in the post body. See section 2.1:
The client makes a token exchange request to the token endpoint with an extension grant type using the HTTP POST method. The following parameters are included in the HTTP request entity-body using the application/x-www-form-urlencoded format with a character encoding of UTF-8 as described in [Appendix B](https://www.rfc-editor.org/rfc/rfc6749#appendix-B) of [[RFC6749](https://datatracker.ietf.org/doc/html/rfc6749)].
then
audience
OPTIONAL. The logical name of the target service where the client intends to use the requested security token. This serves a purpose similar to the resource parameter but with the client providing a logical name for the target service. Interpretation of the name requires that the value be something that both the client and the authorization server understand. An OAuth client identifier, a SAML entity identifier [[OASIS.saml-core-2.0-os](http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)], and an OpenID Connect Issuer Identifier [[OpenID.Core](https://openid.net/specs/openid-connect-core-1_0.html)] are examples of things that might be used as audience parameter values. However, audience values used with a given authorization server must be unique within that server to ensure that they are properly interpreted as the intended type of value. Multiple audience parameters may be used to indicate that the issued token is intended to be used at the multiple audiences listed. The audience and resource parameters may be used together to indicate multiple target services with a mix of logical names and resource URIs.
My presumption was that we'd parse out the requested audience from the body at the same time we parse out the subject_token and optionally the actor_token in the filter flow. We'd then have to stash that somewhere that the knoxtoken service could access for inclusion in the minted token, presuming the request passed filter authz. The commonly used spot seems to be a request header.
So, one possibility is that for RFC 8693 token exchange we require the audience to be specified in the body and reject requests with it in a header or query parameter rather than ignore it. We add it to the request headers if authz passes in the filter. Then for knoxtoken, we can read it from a header rather than a querystring.
If we want to support post body/header in one path and querystring in another, we probably want different feature flags for it. But i'm not aware of any reason to allow the querystring.
| } | ||
|
|
||
| private JWT getJWT(UserContext userContext, long issueTime, long expires, String jku) throws TokenServiceException { | ||
| private List<String> parseRequestedAudiences() { |
There was a problem hiding this comment.
This is where we could pull it from the header instead of the querystring, seems otherwise should just work.
| EasyMock.expect(request.getParameter(TokenResource.QUERY_PARAMETER_DOAS)).andReturn(contextExpectations.get(TokenResource.QUERY_PARAMETER_DOAS)).anyTimes(); | ||
| } | ||
| EasyMock.expect(request.getParameterNames()).andReturn(Collections.emptyEnumeration()).anyTimes(); | ||
| final Map<String, String[]> parameterMap = new HashMap<>(); |
There was a problem hiding this comment.
Possibly in the filter for the rfc token exchange we should check there is no audience querystring param and fail if there is, just to be clear. Also, if we're stashing a value in the header, we should fail if there already is one value there. If we switch knoxtoken to also use a header as suggested, then the former we'd maybe just want to do in general, but the latter we'd leave off if not rfc token exchange.
| private final WhitelistAudienceValidator validator = new WhitelistAudienceValidator(); | ||
|
|
||
| @Test | ||
| public void noRequestedAudienceReturnsConfigured() throws Exception { |
There was a problem hiding this comment.
If the backwards compatible case is 'static' and the 'whitelist' is new, then why not have this return an empty set? That is, the way to get the current behavior is to use the 'static' validator, it just returns what's configured, ignores whatever is requested. If you switch to a whitelist, it means you want to request audiences. It seems like a security hole that if you don't request an audience you get all of them.
I could see it for an incremental adoption use case: some services start requesting audiences, but not all are updated yet. The ones that haven't updated get the same value as the static validator, so remain backwards compatible until they switch.
It's just that after migration is done, anyone that wants to backslide, or new services, can start off without requesting any audiences and get them all, so their token will just work everywhere. So the end state is less secure.
Just an observation, but I'd make sure to document this behavior: you have to check your audit logs for exchanges that don't request audiences after a migration to the 'whitelist' validator is done to ensure folk are requesting appropriate audiences. Or, put in some controls to restrict the number of audiences allowed in a token for validation.
|
|
||
| With the `whitelist` validator its behavior is: | ||
|
|
||
| * the request does not contain an `audience` parameter -> the statically configured `knox.token.audiences` are used, exactly as before (unchanged default behavior) |
There was a problem hiding this comment.
Ah, you did already document it, nice.
|
|
||
| Only exact matches against the whitelist are honored. | ||
|
|
||
| The `passthrough` validator instead stamps the requested audience(s) into the token's `aud` claim verbatim, without any whitelist check, and rejects nothing. If the request contains no `audience` parameter the token is issued with no `aud` claim. Because it performs no local authorization, `passthrough` relies on a downstream JWTProvider to reject tokens whose `aud` does not match the consumer topology's expected audiences, deferring enforcement to the point of consumption. Use it only when such consumption-time validation is in place. |
There was a problem hiding this comment.
I didn't follow this. Isn't the provider upstream from the knoxtoken? That is, if configured in the topology, the filter would run before all this code, and if it failed the audience validation there, no token would be issued, you'd get a failure response. There's no special requirement on consumption -- wherever you send the newly minted token.
What I thought was, something more like:
The passthrough validator is meant for use when a JWTProvider is configured in the topology that performs validation on the requested audience already. Then, any requested audience in KNOXTOKEN will be set in the aud claim of the minted token verbatim without further validation required.
KNOX-3424 - Dynamic audience handling in the KNOXTOKEN service
What changes were proposed in this pull request?
Callers can now request a token's
audclaim per request via anaudiencequery parameter. Requested values are validated by a pluggable audience validator, selected with the newknox.token.audience.validatorinit param, so operators opt into dynamic audiences explicitly and untrusted callers can't spoof arbitrary audiences.Validators
Selected per topology via
knox.token.audience.validator:static(default) — preserves the existing behavior. Theaudiencequery parameter is ignored and the statically configuredknox.token.audiencesare always used. No config change needed for existing deployments.whitelist— honors theaudiencequery parameter, validated against theknox.token.audienceswhitelist.passthrough— accepts whatever audience(s) the caller requests, with no local whitelist. Enforcement is deferred to the consumption-time JWTProvider on the topology fronting the target service.Behavior (whitelist)
audienceparam → the configuredknox.token.audiencesare used (unchanged).audienceparam, all values in the whitelist → only the requested audience(s) land inaud.audienceparam, any value not whitelisted →400.Behavior (passthrough)
audienceparam present → the requested audience(s) are stamped intoaudverbatim; no whitelist check, never rejected.audienceparam → the token is issued with noaudclaim (no fallback toknox.token.audiences, unlikestatic/whitelist).audat consumption time.Fail-fast
whitelistrequiresknox.token.audiencesto be configured — a validator that needs a whitelist but has nothing to validate against fails deployment (ServiceLifecycleException) rather than silently accepting anything. An unknown validator name also fails deployment.Extensibility
AudienceValidator.validateAndResolve(...)is a small strategy interface; new validators can be added and selected throughknox.token.audience.validatorwithout touchingTokenResource.How was this patch tested?
Unit tests, local tests
Integration Tests
N/A
UI changes
N/A