Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple jwt keys for authentication #1374

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

david-mccullars
Copy link

Modify the auth strategy for libsql-server to support multiple JWT keys. This will allow server administrators to more gracefully rotate keys by adding new keys without having to remove old ones simultaneously. Once all clients have been migrated to using new keys, the old one can be safely removed.

The modified auth strategy will accept a JWT if any of the public keys can successfully decrypt it. However, if none can (or all error), the last error will be returned.

As defined in RFC 1422 a *.PEM file by definition can contain multiple items, so to provide multiple JWT keys one needs only to concatenate them together. For example:

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAz78yKnNWXkSQJUdQW2TU3WdelH2KtifEzg27BdtIL7c=
-----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAKg/aT1QW16CtOKFqEvj2kvxODjWOBA6iYPDRnzrbLJ0=
-----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEARi1RRwfK5qpUf1leN9Qtjt9lRIBuNDAz6AE8PdnoSCc=
-----END PUBLIC KEY-----

This should work whether the multiple certificates are passed via the SQLD_AUTH_JWT_KEY environment variable or the --auth-jwt-key-file argument.

jwt_keys: &Vec<jsonwebtoken::DecodingKey>,
jwt: &str,
) -> Result<Authenticated, AuthError> {
for (index, jwt_key) in jwt_keys.iter().enumerate() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be limited to two keys so that no iteration needs to be done and encapsulate the concept some how.

Pseudo Code:

validate_jwt(&primary_jwt_key, jwt) || validate_jwt(&secondary_jwt_key, jwt);

Copy link
Author

Choose a reason for hiding this comment

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

It's a good question. Here was my thinking:

  1. For the (99%) key rotation use case it should only be in this state for a relatively short period of time (maybe a few hours or a day tops). Otherwise it'll be in the singleton case where we use the DecodingKeyContainer::Single to avoid iteration per Turso's suggestion.
  2. There may be use cases that I'm not thinking about where 3 or more keys are actually desired. I couldn't come up with a good reason to artificially block this.

@@ -7,8 +7,13 @@ use crate::{

use super::{UserAuthContext, UserAuthStrategy};

pub enum DecodingKeyContainer {
Copy link
Contributor

@shopifyski shopifyski May 6, 2024

Choose a reason for hiding this comment

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

It won't be long until we forget why we implemented this single/multiple distinction
it is a seed of complexity -> it requires forked logic to handle
any chance we could have one more round of conversations about how much real overhead a Vec<> introduces?

Copy link
Author

Choose a reason for hiding this comment

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

I'm okay removing the complexity if everyone prefers that after looking at the enum version. Could be a classic case of premature optimization. Fwiw, I do think it should remaining somewhat intuitive that the vast majority of the time there should only be a single key and hence a motivation for this bit of weirdness.

Copy link
Contributor

Choose a reason for hiding this comment

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

let's let the others chime in then

Copy link
Collaborator

Choose a reason for hiding this comment

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

I second @shopifyski here, I'd rather have a single code path, the overhead of allocating a vector once in a while is negligible

Copy link
Author

Choose a reason for hiding this comment

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

As an extra data point, I was exploring today what it would look like with just Vec<jsonwebtoken::DecodingKey> everywhere except for the DecodingKeyContainer enum. The immediate instinct was to rename all jwt_key to jwt_keys since it felt weird to do things like pub fn parse_jwt_key(data: &str) -> Result<Vec<DecodingKey>> or let key: Vec<DecodingKey> = .... However, as the thread kept on pulling I started to run into some backwards-compatibility issues, specifically with the jwt_key (singular) request variable in libsql-server/src/http/admin/mod.rs. We can certainly halt the cascade of singular-to-plural changes anywhere we like, but at some point we may have something gross like config.jwt_keys = req.jwt_key.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We could extend the config with jwt_keys, and merge jwt_key and jwt_keys as late as possible when building the decoding object. We would mark the jwt_key field as deprecated, and cleanup sometime in the future, would that contain the changes?

) -> Result<Authenticated, AuthError> {
for (index, jwt_key) in jwt_keys.iter().enumerate() {
let result = validate_jwt(&jwt_key, jwt);
if result.is_ok() || index == jwt_keys.len() - 1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

we're swallowing individual errors
we should at least log a warning if result is Err

Copy link
Author

Choose a reason for hiding this comment

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

The problem is that those errors are expected. If I have two different keys, only one of them can decrypt the JWT, so the other one will error out. If we log them it will get very noisy without adding value.

Copy link
Contributor

Choose a reason for hiding this comment

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

maybe debug log then? to turn it on only when troubleshooting?

}
}

Err(AuthError::Other)
Copy link
Contributor

Choose a reason for hiding this comment

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

in case validation fails, we might be interested in individual errors for each jwt
I suggest that we collect all errors an fold them into a single one, something like https://stackoverflow.com/a/61171459

Copy link
Author

Choose a reason for hiding this comment

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

When validation fails, we'll return the last error. The only reason we would ever get this generic one is when there are no JWT keys - which really shouldn't happen.

Copy link
Author

Choose a reason for hiding this comment

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

Another thought here is do we want to expose the fact that there are multiple keys to clients? That feels like an internal piece of data that really shouldn't be leaked.

@@ -138,6 +164,23 @@ mod tests {
(encoding_key, decoding_key)
}

fn key_pairs(
Copy link
Contributor

@shopifyski shopifyski May 7, 2024

Choose a reason for hiding this comment

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

maybe rename to generate_key_pairs
verb_object() naming convention is preferred for functions
key_pair() function should also be renamed
object() only names are best left for getters

Copy link
Author

Choose a reason for hiding this comment

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

I'm okay with renaming both, but as you suggest we should keep them consistent. It would be weird to have one be key_pair and the other generate_key_pairs.

@david-mccullars david-mccullars force-pushed the support-multiple-jwt-keys branch 2 times, most recently from 04a8422 to b54dd01 Compare May 9, 2024 00:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants