Closed woodruffw closed 1 month ago
For (2), we probably need either a single new table with a unique constraint on (provider-name, jti) or a new table per-provider with a unique constraint on jti. With both, the table will probably have unbounded growth unless we also periodically delete it (on the same cadence as our self-expiring API tokens, maybe?)
If we make this (provider-name, jti, exp)
, we could just periodically prune all tokens past their expiration date without worrying about provider-specific token lifetime lengths.
Google Cloud supports nonce, which is subtly different (jti is server controlled, nonce is user-controlled)
Just double-checked, and Google Cloud does not support either jti
or nonce
in the OIDC credential retrieval flow that we use.
@th3coop could you provide an example of the claim set that ActiveState offers in its JWTs? I'm curious if your claims include a jti
we could use π
I've asked @DarkaMaul to look at this!
Hey morning @woodruffw, sounds like our docs aren't as navigable as they should be. The supported claims (though not worded as such) are here: https://docs.activestate.com/platform/user/oidc/#understanding-the-token We also have our .well-known/openid-configuration
So we support nonce
by passing it along in the OIDC JWT if it's present in the API request to create said token.
We do not currently support jti
.
For (2); I've done a similar entry tracking, validation and expiry backed by Redis before.
Pros:
Caveats:
Just an idea. No sweat if not used/totally ignored :D.
Don't hesitate to ping me if you need more info about the ActiveState stuff or if you'll need ActiveState to put up a PR or anything else I can assist with.
Got it, thanks @th3coop! I swear I've seen that page before, but for some reason I couldn't find it when searching.
We do not currently support jti.
Not to snipe you into adding this, but how difficult do you think it would be to add to your claims? 16 bytes of CSPRNG should be more than sufficient.
Re: Redis: that is probably workable! We definitely already have a Redis service in Warehouse's deployment, and it already provides JWKS caching/rotation for the verification phase.
I don't have a super strong intuition for whether that would be better or worse than a new DB table, though -- the table would be "cheap" here in the sense that it would have no relations, but still heavier weight than Redis. OTOH, with Redis, I think we'd need to keep a potentially unbounded number of JWTs resident in RAM, right?
(Technically the number of JWKS is also unbounded, but we currently pull those from trusted parties, i.e. the OIDC IdPs we trust.)
The hard/longest part of the task will be me second guessing myself on picking the correct algorithm π. I'd probably just slap a UUID in there since we use them for everything already. Other than that, it would not be hard for us to add to out OIDC JWT tokens.
What kind of timeline are you all hoping for here?
Re: Redis: When you say "potentially unbounded number" you mean that there could be any number of values stored at any given time with no guarantee that you have enough space, correct? Then definitely yeah. You need estimates on how many requests you expect to get over the expiry time of a these OIDC tokens, which should be quite short compared to out JWTs. Would you be able to scale vertically, adding memory, if volume spiked for some reason?
The hard/longest part of the task will be me second guessing myself on picking the correct algorithm π. I'd probably just slap a UUID in there since we use them for everything already. Other than that, it would not be hard for us to add to out OIDC JWT tokens.
UUIDv4 has 122 bits of entropy when generated correctly, so this would be more than sufficient π
What kind of timeline are you all hoping for here?
No hard timeline for this, IMO -- the top level feature would be a good one for PyPI to have, but it's currently not breaking anything and can be rolled out incrementally for each IdP. So it'd be great to have ActiveState support jti
, but a lack of support wouldn't prevent implementation from rolling forwards here.
Re: Redis: When you say "potentially unbounded number" you mean that there could be any number of values stored at any given time with no guarantee that you have enough space, correct?
Yep. That part was mostly me thinking out loud -- I suspect in practice this wouldn't be a huge issue, but AFAIK there aren't many other places where user interaction with PyPI can cause unbounded Redis usage, so I figured it was worth calling out.
That all makes sense. I don't see problem getting this added to our OIDC token pretty quickly when the time comes. Keep my posted and please let me know if I can help in other ways, if I can, I will.
Hi - I'm taking over this issue.
For the moment, the plan is as follows:
Create a oidc_jti_tokens table with :
Create a task to periodically clean this table (daily) if expiration is after current timestamp : delete_expired_jwt_tokens
Before minting a token, in verify_claims
, if the publisher is an OIDCPublisher
, check that the current JTI is not already present in the oidc_jti_tokens
table. If not, bail out with an error :
If all claims checks are successful, add the current jti to the oidc_jti_tokens
table with the expiration date set to the exp of the JWT.
Add tests for all this
Let me know if you have any feedback on how to improve this.
Create a oidc_jti_tokens table with : Create a task to periodically clean this table (daily) if expiration is after current timestamp : delete_expired_jwt_tokens
Instead of this, I like @th3coop's idea here:
For (2); I've done a similar entry tracking, validation and expiry backed by Redis before.
E.g., using the EXPIRE command to let redis automatically expire these keys.
Create a oidc_jti_tokens table with : Create a task to periodically clean this table (daily) if expiration is after current timestamp : delete_expired_jwt_tokens
Instead of this, I like @th3coop's idea here:
For (2); I've done a similar entry tracking, validation and expiry backed by Redis before.
E.g., using the EXPIRE command to let redis automatically expire these keys.
Sure, I'll implement the Redis version first, and we can replace it with a table if needed afterwards.
If you use the PXAT
argument on the SET
command when adding the token to Redis, I think you'll be able to pass the OIDC tokens exp
field as the expiry time/date. https://redis.io/docs/latest/commands/set/#options
I think it should be just EXAT
instead of PXAT
since we don't have millisecond resolution, but thanks for the pointer to these options!
I hate to necromancer a closed issue -- I'm happy to create a new one if I'm on the right track! -- but I'm seeing what looks like a failure of GitHub actions' tokens to include a JTI: https://github.com/offbyone/django-svcs/actions/runs/10552758336/job/29233437205
##[debug]Authenticating to https://test.pypi.org/legacy/ via Trusted Publishing
##[debug]Selected Trusted Publishing Exchange Endpoint: https://test.pypi.org/_/Oidc/Mint-Token
Error: Trusted publishing exchange failure:
Token request failed: the server refused the request for the following reasons:
* `invalid-publisher`: valid token, but no corresponding publisher (Missing claim 'jti')
This generally indicates a trusted publisher configuration error, but could
also indicate an internal error on GitHub or PyPI's part.
This is in reference to test pypi, not primary, so it's possible this is something less critical, but it's biting me as I try to publish a new package for the first time using a trusted publisher.
@offbyone Huh that is not something we expected. I tried to reproduce it but couldn't.
Looking at warehouse
's code, I don't see any obvious things that would trigger that error other than a missing jti
claim. Could you try re-running that workflow without any changes? If it's reproducible, we can try to extract the OIDC token and see what's going wrong.
Happy to, here ya are: https://github.com/offbyone/django-svcs/actions/runs/10552758336
(if you'd prefer I open a new issue for this, just say the word)
(if you'd prefer I open a new issue for this, just say the word)
Yeah go for it, better to have this problem documented in its own issue
Pointed out by @sethmlarson and @di: an OIDC credential can currently be re-used to issue multiple temporary API tokens, for as long as its expiry window remains open. This results in two potential weaknesses:
(2) is unlikely on GitHub, since any context in which the attacker has access to the OIDC credential is also likely one in which they can mint a fresh credential. But this may be still relevant for other supported OIDC IdPs.
Both of these can be addressed by enforcing one-time use of the OIDC credential within Warehouse:
jti
claim in their JWTs.jti
s: if the same JTI is seen again, the JWT it belongs to should be rejected and token exchange should fail.For (1), both GitLab and GitHub support
jti
. Google Cloud supportsnonce
, which is subtly different (jti
is server controlled,nonce
is user-controlled). I'm not 100% sure what ActiveState since their docs don't mention any claims explicitly πFor (2), we probably need either a single new table with a unique constraint on
(provider-name, jti)
or a new table per-provider with a unique constraint onjti
. With both, the table will probably have unbounded growth unless we also periodically delete it (on the same cadence as our self-expiring API tokens, maybe?)