Repository navigation
Conversation
…ical non-general flask validation. more tests
…n be passed from config)
|
The style in this PR agrees with This formatting comment was generated automatically by a script in uc-cdis/wool. |
Coverage Report for CI Build 37072616144Coverage increased (+20.0%) to 80.283%Details
Uncovered ChangesNo uncovered changes found. Coverage Regressions34 previously-covered lines in 4 files lost coverage.
Coverage Stats
💛 - Coveralls |
| raise ValueError( | ||
| f"htu mismatch: request URL '{actual_url}' != proof htu '{htu_value}'" | ||
| ) |
There was a problem hiding this comment.
I got this error during my testing, with:
I fixed it on my side by getting rid of the double slash, but maybe there's a way to make this more robust
There was a problem hiding this comment.
imo it shouldn't be more robust here. b/c technically, foobar//outputs/img-1.png is a different resource than foobar/outputs/img-1.png. We tend to be forgiving in our APIs with trailing slashes, but empty resource paths are legitimately allowed and are technically different resources.
Do you know why that happened in the first place? I'd rather fix the source of accidental double-slashes than make the validation itself more flexible.
There was a problem hiding this comment.
I added an extra slash when building the key in my call to (boto3) s3_client.get_object(bucket, key)
There was a problem hiding this comment.
Leaving as-is unless you think we shouldn't. working on adding percent decoding based on feedback from the gen3 workflow PR though
There was a problem hiding this comment.
I'm still a bit unsure about this one but you know dpop better so i trust your judgement
nss10
left a comment
There was a problem hiding this comment.
Some questions and minor change suggestions
| f"aud must be string, list or None. Instead received aud of type {type(aud)}" | ||
| ) | ||
| if not isinstance(scope, set) and not isinstance(scope, list) and not scope is None: | ||
| if not isinstance(scope, set) and not isinstance(scope, list) and scope is not None: |
There was a problem hiding this comment.
| if not isinstance(scope, set) and not isinstance(scope, list) and scope is not None: | |
| if scope is not None and not isinstance(scope,(set,list)): |
There was a problem hiding this comment.
good call, will fix
| # get rid of any prefixed 'DPoP ' / 'dpop ' | ||
| dpop_header = _strip_auth_scheme(dpop_header) |
There was a problem hiding this comment.
Why do it twice? Is this for future proofing?
Currently, extract_and_validate_jwk is called only once in the entire library, and the dpop_header used there has already stripped away the auth scheme.
There was a problem hiding this comment.
it's for direct callers. extract_and_validate_jwk is public for clients to call, so someone can hand it the raw DPoP eyJ... header value and it should still work. the strip up in validate_dpop_proof is needed separately b/c _verify_signature_and_claims uses dpop_header too. calling it on an already-stripped value is a no-op (returns early when there's no space), so it should be safe
There was a problem hiding this comment.
I noticed the same thing. I was going to comment that you can remove the _strip_auth_scheme call from validate_dpop_proof since it's called in extract_and_validate_jwk
| if "exp" in claims_dict: | ||
| if current_time >= claims_dict["exp"]: | ||
| raise ValueError("Invalid DPoP proof: expired") |
There was a problem hiding this comment.
| if "exp" in claims_dict: | |
| if current_time >= claims_dict["exp"]: | |
| raise ValueError("Invalid DPoP proof: expired") | |
| if "exp" in claims_dict and if current_time >= claims_dict["exp"]: | |
| raise ValueError("Invalid DPoP proof: expired") |
There was a problem hiding this comment.
cleaner 👍 will apply by hand b/c the suggestion has a stray if
|
|
||
| # Normalize BOTH sides so that an explicit default port (e.g. :443 added by | ||
| # a reverse proxy) does not cause a mismatch. | ||
| actual_url = _get_url(request_url) |
There was a problem hiding this comment.
Why a new actual_url variable? Can't we simply reuse request_url?
There was a problem hiding this comment.
actual_url isn't the same value as request_url. It's _get_url(request_url), so the query and fragment are gone, the scheme and host are lowercased, and a default :443/:80 is dropped. Both the comparison and the error message use that normalized form. We could write request_url = _get_url(request_url) instead, but then request_url would mean the raw input above that line and the normalized value below it, which is harder to follow. I'd rather keep them separate. I can rename it to normalized_request_url if that reads better
| algorithm" - which includes symetric algs not allowed. | ||
| """ | ||
|
|
||
| max_header_length = 4096 |
There was a problem hiding this comment.
Shouldn't this be in sync with MAX_DPOP_HEADER_LENGTH
There was a problem hiding this comment.
they bound different things - MAX_DPOP_HEADER_LENGTH is the whole header value (header.payload.signature) checked before we parse anything, while joserfc's max_header_length only applies to the first part of the JWT (the header). so it has to be smaller. will add to the docstring so that's clear
| assert ticks > 5, f"event loop appears to have been blocked (ticks={ticks})" | ||
|
|
||
|
|
||
| _JWKS_URL = "https://example.com/issuer/.well-known/jwks.json" |
There was a problem hiding this comment.
You might want to hoist this to the top of the file, since it is used above.
There was a problem hiding this comment.
good catch, will move it up
|
|
||
|
|
||
| @pytest.mark.anyio | ||
| @pytest.mark.anyio |
| result1 = await _get_key(tokens[0], allowed_issuers=issuer) | ||
| result2 = await _get_key(tokens[1], allowed_issuers=issuer) |
There was a problem hiding this comment.
Super minor! But, result1 and result2 are never used. Maybe add another assert verifying their values? or maybe we can skip storing them altogether. maybe?
There was a problem hiding this comment.
will drop these here and add a new test. these tests are about cache, and both kids share the same key material in this fixture, so asserting on the values wouldn't prove anything
Co-authored-by: Sai Shanmukha Narumanchi <nss10@outlook.com>
Co-authored-by: Sai Shanmukha Narumanchi <nss10@outlook.com>
Co-authored-by: Sai Shanmukha Narumanchi <nss10@outlook.com>
|
I did not review the tests |
| [tool.pytest.ini_options] | ||
| # Every async test runs on asyncio without a per-test mark. The library is | ||
| # asyncio-only (token/fastapi.py depends on asyncio directly). | ||
| asyncio_mode = "auto" |
paulineribeyre
left a comment
There was a problem hiding this comment.
lgtm, did not review the tests
New Features
Breaking Changes
core.validate_jwt:issuersrenamed toallowed_issuers, now required andrejected when empty - there is no way to skip the issuer check.
audandscoperemain required, as before. Newpurposeanddenylist_callbackparams.core.validate_jwtpositional slots past 5 shifted: slot 6 wasoptions, nowpurpose; slot 7 waslogger, nowoptions. Move those to keywords.token.validate.validate_jwtnow raisesValueErrorwhen none ofOIDC_ISSUER/USER_API/BASE_URLis configured, instead of failing every tokenwith
JWTError.JWTAudienceErrorreportsgot unknown;JWTExpiredError→"token has expired"). Don't match on text.kidthe issuer does not publish is an error ratherthan falling back to another key.
Bug Fixes
token/fastapi.py: the issuer derived from the first request leaked into everylater request, so one token with a junk
isswould 403 all subsequent validtokens for the life of the process. Now per-request.
n/eare strictly base64url-decoded. Previously a malformed JWKS silentlyproduced a junk key, so every token read as "bad signature" rather than
"issuer published a bad key".
keys.pyhandles both.well-knownJWKS and legacy Fence/jwt/keysformats;10s timeout on key discovery; the public-key cache is mutex-guarded.
Improvements
httpx(no longer developed) withhttpx2.Dependency updates
joserfc >=1.7.3(+truststore, transitive via httpx2)httpx >=0.23,<1.0→httpx2 >=2.9.1;pyjwt >=2.4.0→>=2.11.0Deployment changes
DPOP_SHARED_SECRET- required by any service that mints or verifies DPoPnonces, MUST BE THE SAME value across ALL participating services.
DPOP_NONCE_TTL- optional, seconds, default 300.