Uninett / nav

Network Administration Visualized
GNU General Public License v3.0
181 stars 37 forks source link

Replace current API token implemention with a JWT implementation #2481

Open lunkwill42 opened 1 year ago

lunkwill42 commented 1 year ago

Is your feature request related to a problem? Please describe.

As of version 5.4, API tokens are generated and persisted in the NAV database. A token's expiry time and access claims are stored in the database, and can be changed at any time by an administrator, without changing the token itself.

One problem with this solution is that there is no automated way to renew a token. Any token renewal must be managed manually by a NAV administrator in the "User & API Admin" UI . There is no explicit renewal mechanism in this UI, which is usually worked around by admins by extending the expiry time of an existing token. This more or less leads to tokens that never expire. The risk of a token leak increases with extended token lifetime.

Another problem is that there is no way to delegate API access authorization to a third party system, which is desirable in some situations (such as using FEIDE to perform federated API authorization).

Describe the solution you'd like

We'd like NAV to support JSON Web Tokens (JWT, RFC 7519) instead of today's solution.

Some requirements:

Describe alternatives you've considered

None, at the moment.

Breakdown

lunkwill42 commented 1 year ago

More requirements will probably come. This issue should probably be broken down into smaller parts.

hmpf commented 1 year ago

drf-simplejwt has a StatelessToken backend that does not talk with the user table, considering that nav does not use django.contrib.auth.

stveit commented 1 year ago

Thoughts on refresh tokens:

Users should be able to generate refresh token together with an access token.

The API should have an endpoint that accepts valid refresh tokens and returns a relevant access token together with a new refresh token. Refresh tokens should preferably only be used once. The new refresh token can also have a refreshed expiry date. This would make it so you can generate tokens into infinity as long as you keep doing it before your refresh token expires. This means if an attacker gets a valid refresh token they can also gain infinite access. The possibility of blacklisting refresh tokens can help mitigate this. If you can somehow detect if a refresh token is used more than once, then this might be a sign that a token is stolen.

Can potentially have a separate expiry date where refresh will no longer work. This date would be relative to the original refresh token, so no matter how many times you refresh there is a hard limit where all refresh tokens derived from an original refresh token will no longer work. This is used in an old django jwt module

lunkwill42 commented 1 day ago

Thoughts on refresh tokens:

I'm not sure like the infinte refresh tokens solution any more than I like the potential current practice of renewing existing opaque access tokens indefinitely, @stveit

The only concrete Oauth2 example I have any direct experience with are the tokens issued by Microsoft Outlook to access IMAP and SMTP. I like these. To get a refresh token, you need to authenticate using a username and a password (and potentially 2FA). The refresh token has a long, but finite expiry time (maybe 90 days, IIRC). Access tokens only have about 30 minutes or so expiry time, before you need to fetch a new one using the refresh token. The refresh token stays the same.

If we want to avoid infinite refresh tokens (I think we do), it would be easier to implement a solution with long-lived finite refresh tokens, than to enforce a out-of-band limit on how many times you can refresh the refresh token. I'm not sure what extra security would be gained by allowing refreshable refresh tokens in that scenario (but I'm not a security researcher).

lunkwill42 commented 1 day ago

I also do think we may need some way to manually revoke refresh tokens. Normally, if tokens were tied to user accounts (which they are not in NAV, not yet anyway), if the user lost access, their next attempt to renew would be denied. Since NAV tokens aren't tied directly to the user, but issued by an admin, the admin would need some way to manually revoke tokens.

In our scenario, I'm not sure how you can do revocation properly without storing refresh tokens. Perhaps by giving each token an identifier that doesn't contain the whole token, but is contained within the token?

stveit commented 1 day ago

its a bit awkward, JWT certainly works best when you have a dedicated issuer like Feide to work with.

An identifier might work, but its essentially the same as storing the entire token, just a bit lighter weight. An advantage of storing the entire refresh token is that you dont really need to validate it and check for signature. If someone has a token thats identical to the stored token, its bound to be real, so you save some crypto overhead there.

We should have a meeting and just make a decision on what we should do

lunkwill42 commented 42 minutes ago

its a bit awkward, JWT certainly works best when you have a dedicated issuer like Feide to work with.

An identifier might work, but its essentially the same as storing the entire token, just a bit lighter weight. An advantage of storing the entire refresh token is that you dont really need to validate it and check for signature. If someone has a token thats identical to the stored token, its bound to be real, so you save some crypto overhead there.

I'm not comfortable with storing the full tokens in cleartext the database - that's what we're trying to get away from here. However, storing a hash of the refresh token could do. You would need to hash the incoming token and compare it with the existing token hashes in the database - then, at least, no tokens could leak from the database if anyone got unauthorized access to it.

We should have a meeting and just make a decision on what we should do

Sure, a little design pow-wow wouldn't be too off.