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

feat(gateway): add JWT authentication #6535

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

Conversation

f3r10
Copy link

@f3r10 f3r10 commented Dec 10, 2024

closes #4128

Motivation

Add support for JWT gateway authentication

This PR adds one additional endpoint:

  • /auth/session if the password is correct, this endpoint returns a JWT token that must be used in the other endpoints that require authentication.

How to test it using the CLI:

gateway-cli --address=http://127.0.0.1:16265/v1 --rpcpassword=theresnosecondbest get-session-jwt-auth

gateway-cli --address=http://127.0.0.1:16265/v1 --rpcjwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAzY2I1MTBjOWJiMzA5ODNkOTYzODk3ZWUyNjlkM2FiZmY3Nzg0YTVjMmVkYWM3YjQzMzg1NjIyMmMzZGIxYWQ1ZSIsImV4cCI6MTczMzg1ODUxN30.4FNxyn6Y-M_vfPRstxgrv4weK9MMRv31Inijw6JJo80 get-balances

Copy link
Contributor

@elsirion elsirion left a comment

Choose a reason for hiding this comment

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

Thx for the PR, logic looks good, just code organization could be improved. Generally I'd recommend to think about how arguments to functions can be reduced, that way you typically get a good feeling for where a function should live (e.g. verification belongs into AuthManager because it has access to the secret and that's all you need to verify).

Comment on lines +58 to +63
cli.rpcpassword.clone(),
cli.rpcjwt.clone(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Now needing both is unfortunate, ideally the password would just be an implementation detail of some function/struct providing fetching the session token. What's the plan to evolve this?

I see, both are optional.

Comment on lines 376 to 382
let gateway_id = Self::load_or_create_gateway_id(&gateway_db).await;

// for JWT it is necessary to create an encoding secret that will be used each
// time a new JWT token is generated. The encoding secret ensures token's
// integrity and authenticity, so it is necessary to use a secure random
// number generator to generate strong keys.
let encoding_secret: [u8; 16] = rand::thread_rng().gen();
Copy link
Contributor

Choose a reason for hiding this comment

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

The secret being genearted on the fly means a GW restart invalidates old tokens I assume? Not a problem imo, just want to point it out so everyone is aware.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, you are right. Where should I put a note about that? Would it be okay as an additional comment, or should I create an additional document for that?

}

impl GatewayRpcClient {
pub fn new(versioned_api: SafeUrl, password: Option<String>) -> Self {
pub fn new(versioned_api: SafeUrl, password: Option<String>, jwt_code: Option<String>) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

What are the semantics of providing a password, a JWT or both? Please document. E.g. does providing the password lead to automatic requesting of a JWT whenever the previous expires?

Copy link
Author

Choose a reason for hiding this comment

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

Providing both a password and a JWT does not make sense.
Providing an initial password has the purpose of generating a JWT that has to be used to request private endpoints.

Maybe instead of providing optional parameters (JWT and password) would be better to provide an enum for each case, WDYT?

}
return Ok(next.run(request).await);
}
//TODO remove this fallback when all the callers support JWT auth
Copy link
Contributor

Choose a reason for hiding this comment

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

Should create an issue once merged

Copy link
Author

Choose a reason for hiding this comment

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

yes, @elsirion it would be better to create additional issues in this repository, and also I think it would be necessary to create an issue in the ui repository.

Comment on lines 97 to 109
let auth_manager = gateway.auth_manager.lock().await;
if let Ok(decoded_token_data) = jsonwebtoken::decode::<crate::auth_manager::Session>(
&token,
&jsonwebtoken::DecodingKey::from_secret(auth_manager.encoding_secret.as_ref()),
&jsonwebtoken::Validation::default(),
) {
let now = fedimint_core::time::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
if now > decoded_token_data.claims.exp {
return Err(StatusCode::UNAUTHORIZED);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a separate function for checking the JWT, would make the code cleaner.

EDIT: should live inside AuthManager after reading the full diff

@@ -197,6 +199,9 @@ pub struct Gateway {
/// The gateway's federation manager.
federation_manager: Arc<RwLock<FederationManager>>,

/// The gateway's authentication manager
auth_manager: Arc<Mutex<AuthManager>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does the auth manager need to be mutated at all? Can we remove the Acr<Mutex<_>>?

Comment on lines 131 to 136
let session = auth_manager
.generate_session()
.map_err(|_| AdminGatewayError::Unauthorized)?;
let token = session
.encode_jwt(&auth_manager.encoding_secret)
.map_err(|_| AdminGatewayError::Unauthorized)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be encapsulated as one function in the AuthManager. There's no reason to generate a session without signing it imo. Just call the fn something that makes it clear that the session tokens are signed.

Comment on lines 31 to 64
pub struct Session {
/// the unique identifier of the session.
pub id: PublicKey,
/// the expire time of the session
pub exp: u64,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Member fields can be private afaik. Only the auth manager should access them to either create a session or check it.

Comment on lines 39 to 76
pub fn new(id: PublicKey, expiry: u64) -> Self {
let now = fedimint_core::time::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
Self {
id,
exp: now + expiry,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This could become private too

Comment on lines 50 to 58
pub fn encode_jwt(self, encoding_secret: &[u8; 16]) -> anyhow::Result<String> {
let claim = self;
encode(
&Header::default(),
&claim,
&EncodingKey::from_secret(encoding_secret),
)
.map_err(|_| anyhow::anyhow!("Unable to generate jwt token session"))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be moved to AuthManager and used inside generate_and_sign_session

@f3r10 f3r10 force-pushed the add_jwt_auth_method branch from acdfd59 to 34a4808 Compare December 18, 2024 19:27
@f3r10 f3r10 requested a review from elsirion December 18, 2024 19:29
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.

Use session tokens for admin API auth
2 participants