Skip to content

Commit

Permalink
Extract JWT to it's own file (#317)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton authored Nov 2, 2023
1 parent a0bfa3c commit eefc58b
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 57 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"Pbkdf",
"PKCS8",
"repr",
"reqwest",
"schemars",
"uniffi",
"wordlist"
Expand Down
71 changes: 71 additions & 0 deletions crates/bitwarden/src/auth/jwt_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::str::FromStr;

use base64::Engine;

use crate::{error::Result, util::BASE64_ENGINE};

/// A Bitwarden secrets manager JWT Token.
///
/// References:
/// - <https://github.com/bitwarden/server/blob/419760623a7af5f5d0cbdfeb2a7aba8c3608d880/src/Identity/IdentityServer/ClientStore.cs#L125-L126>
/// - <https://github.com/bitwarden/server/blob/419760623a7af5f5d0cbdfeb2a7aba8c3608d880/src/Identity/IdentityServer/ClientStore.cs#L133>
///
/// TODO: We need to expand this to support user based JWT tokens.
#[derive(serde::Deserialize)]
pub struct JWTToken {
pub sub: String,
pub email: Option<String>,
pub organization: Option<String>,
pub scope: Vec<String>,
}

impl FromStr for JWTToken {
type Err = crate::error::Error;

/// Parses a JWT token from a string.
///
/// **Note:** This function does not validate the token signature.
fn from_str(s: &str) -> Result<Self> {
let split = s.split('.').collect::<Vec<_>>();
if split.len() != 3 {
return Err(crate::error::Error::Internal(
"JWT token has an invalid number of parts",
));
}
let decoded = BASE64_ENGINE.decode(split[1])?;
Ok(serde_json::from_slice(&decoded)?)
}
}

#[cfg(test)]
mod tests {
use crate::auth::jwt_token::JWTToken;

#[test]
fn can_decode_jwt() {
let jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQz\
g1REJEOUFSUzI1NiIsInR5cCI6ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJu\
YmYiOjE2NzUxMDM1NzcsImV4cCI6MTY3NTEwNzE3NywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI\
6IndlYiIsInN1YiI6ImUyNWQzN2YzLWI2MDMtNDBkZS04NGJhLWFmOTYwMTJmNWE0MiIsImF1dGhfdGltZSI6MTY3NT\
EwMzU0OSwiaWRwIjoiYml0d2FyZGVuIiwicHJlbWl1bSI6ZmFsc2UsImVtYWlsIjoidGVzdEBiaXR3YXJkZW4uY29tI\
iwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IkUzNElDWVhRUFRDS01EVldBREZDNktHNDJCQldJRDdJIiwi\
bmFtZSI6IlRlc3QiLCJvcmdvd25lciI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImRldml\
jZSI6Ijg5Mjg5M2FiLWRkNDMtNDUwYS04NGI1LWFhOWM1YjdiYjJkOCIsImp0aSI6IkEzMkVFNjY5NDdEQzlDNUE2MT\
IwRURBRTIwNzc5OUJFIiwiaWF0IjoxNjc1MTAzNTc3LCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhb\
XIiOlsiQXBwbGljYXRpb24iXX0.AyDkKvjmyaSPQViQSa2sGTKIkDGrUAtDmwpE57K4DDWT0QvwDe7FMktmwiF4LH36\
wx_FnpH21VI1pzwJeTHXtaz3niANJtQZjzGFsNAna_95vrsxZC2YizgGlt6mX4YIGmAw9DiYrmaN0BvQOEm_caV_u6f\
a30iz9Kvjxf7cpzeZvPEysxGpB3k3TRYTkFUdV43HiXdhXMBhyyOpFU6Fk6yA41y7-8bGYc5mYGknWktmPD9Yx-1xKL\
ftFja1SnCoLPWvDeK60lqWZQiT4tZHCYJ7m0bBNCccYHc2Kk2Bo5-UoyDxazPwsqMxeNfjlaUuj3o5N_uQ-4n_gVbeA\
qWV2wrel5UhYjWnczMSLBtt9p0W35kkBPt3ZAnRWMtQMPNH04p-_L6cG-Xu6lDksBTwaavcmtnCKG8V91826EiQ8MrF\
wGWQRZV6tPKTDAYCgSAZGBY3QDmPGT5BeFcg5Ag_nYYIIifKP-kv10v_N-TOcT3NeGBOUlAZ-9m7iT7Rk3vC--SDZdA\
U5turoBFiiPL2XXfAjM7P0r7J91gfXc0FaD6I2jDxOmym5h7Yn5phLsbC2NlIXkZp54dKHICenPl4ve6ndDIJacVeS5\
f3LEddAPV8cAFza4DjA8pZJLFrMyRvMXcL_PjKF8qPVzqVWh03lfJ4clOIxR2gOuWIc902Y5E";

let token: JWTToken = jwt.parse().unwrap();
assert_eq!(token.sub, "e25d37f3-b603-40de-84ba-af96012f5a42");
assert_eq!(token.email.as_deref(), Some("[email protected]"));
assert_eq!(token.organization.as_deref(), None);
assert_eq!(token.scope[0], "api");
assert_eq!(token.scope[1], "offline_access");
}
}
5 changes: 3 additions & 2 deletions crates/bitwarden/src/auth/login/access_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ use crate::{
auth::{
api::{request::AccessTokenRequest, response::IdentityTokenResponse},
login::{response::two_factor::TwoFactorProviders, PasswordLoginResponse},
JWTToken,
},
client::{AccessToken, LoginMethod, ServiceAccountLoginMethod},
crypto::{EncString, KeyDecryptable, SymmetricCryptoKey},
error::{Error, Result},
util::{decode_token, BASE64_ENGINE},
util::BASE64_ENGINE,
Client,
};

Expand Down Expand Up @@ -44,7 +45,7 @@ pub(crate) async fn access_token_login(

let encryption_key = SymmetricCryptoKey::try_from(encryption_key.as_slice())?;

let access_token_obj = decode_token(&r.access_token)?;
let access_token_obj: JWTToken = r.access_token.parse()?;

// This should always be Some() when logging in with an access token
let organization_id = access_token_obj
Expand Down
4 changes: 2 additions & 2 deletions crates/bitwarden/src/auth/login/api_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ use crate::{
auth::{
api::{request::ApiTokenRequest, response::IdentityTokenResponse},
login::{response::two_factor::TwoFactorProviders, PasswordLoginResponse},
JWTToken,
},
client::{LoginMethod, UserLoginMethod},
crypto::EncString,
error::{Error, Result},
util::decode_token,
Client,
};

Expand All @@ -23,7 +23,7 @@ pub(crate) async fn api_key_login(
let response = request_api_identity_tokens(client, input).await?;

if let IdentityTokenResponse::Authenticated(r) = &response {
let access_token_obj = decode_token(&r.access_token)?;
let access_token_obj: JWTToken = r.access_token.parse()?;

// This should always be Some() when logging in with an api key
let email = access_token_obj
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden/src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
pub(super) mod api;
#[cfg(feature = "internal")]
pub mod client_auth;
mod jwt_token;
pub mod login;
#[cfg(feature = "internal")]
pub mod password;
pub mod renew;
pub use jwt_token::JWTToken;

#[cfg(feature = "internal")]
mod register;
Expand Down
53 changes: 0 additions & 53 deletions crates/bitwarden/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ use std::num::NonZeroU32;
use base64::{
alphabet,
engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
Engine,
};

use crate::error::Result;

pub fn default_pbkdf2_iterations() -> NonZeroU32 {
NonZeroU32::new(600_000).unwrap()
}
Expand All @@ -24,63 +21,13 @@ pub fn default_argon2_parallelism() -> NonZeroU32 {
NonZeroU32::new(4).unwrap()
}

#[derive(serde::Deserialize)]
pub struct JWTToken {
pub sub: String,
pub email: Option<String>,
pub organization: Option<String>,
pub scope: Vec<String>,
}

const BASE64_ENGINE_CONFIG: GeneralPurposeConfig = GeneralPurposeConfig::new()
.with_encode_padding(true)
.with_decode_padding_mode(DecodePaddingMode::Indifferent);

pub const BASE64_ENGINE: GeneralPurpose =
GeneralPurpose::new(&alphabet::STANDARD, BASE64_ENGINE_CONFIG);

pub fn decode_token(token: &str) -> Result<JWTToken> {
let split = token.split('.').collect::<Vec<_>>();
if split.len() != 3 {
return Err(crate::error::Error::Internal(
"JWT token has an invalid number of parts",
));
}
let decoded = BASE64_ENGINE.decode(split[1])?;
Ok(serde_json::from_slice(&decoded)?)
}

#[cfg(test)]
mod tests {
#[test]
fn can_decode_jwt() {
let jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQz\
g1REJEOUFSUzI1NiIsInR5cCI6ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJu\
YmYiOjE2NzUxMDM1NzcsImV4cCI6MTY3NTEwNzE3NywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI\
6IndlYiIsInN1YiI6ImUyNWQzN2YzLWI2MDMtNDBkZS04NGJhLWFmOTYwMTJmNWE0MiIsImF1dGhfdGltZSI6MTY3NT\
EwMzU0OSwiaWRwIjoiYml0d2FyZGVuIiwicHJlbWl1bSI6ZmFsc2UsImVtYWlsIjoidGVzdEBiaXR3YXJkZW4uY29tI\
iwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IkUzNElDWVhRUFRDS01EVldBREZDNktHNDJCQldJRDdJIiwi\
bmFtZSI6IlRlc3QiLCJvcmdvd25lciI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImRldml\
jZSI6Ijg5Mjg5M2FiLWRkNDMtNDUwYS04NGI1LWFhOWM1YjdiYjJkOCIsImp0aSI6IkEzMkVFNjY5NDdEQzlDNUE2MT\
IwRURBRTIwNzc5OUJFIiwiaWF0IjoxNjc1MTAzNTc3LCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhb\
XIiOlsiQXBwbGljYXRpb24iXX0.AyDkKvjmyaSPQViQSa2sGTKIkDGrUAtDmwpE57K4DDWT0QvwDe7FMktmwiF4LH36\
wx_FnpH21VI1pzwJeTHXtaz3niANJtQZjzGFsNAna_95vrsxZC2YizgGlt6mX4YIGmAw9DiYrmaN0BvQOEm_caV_u6f\
a30iz9Kvjxf7cpzeZvPEysxGpB3k3TRYTkFUdV43HiXdhXMBhyyOpFU6Fk6yA41y7-8bGYc5mYGknWktmPD9Yx-1xKL\
ftFja1SnCoLPWvDeK60lqWZQiT4tZHCYJ7m0bBNCccYHc2Kk2Bo5-UoyDxazPwsqMxeNfjlaUuj3o5N_uQ-4n_gVbeA\
qWV2wrel5UhYjWnczMSLBtt9p0W35kkBPt3ZAnRWMtQMPNH04p-_L6cG-Xu6lDksBTwaavcmtnCKG8V91826EiQ8MrF\
wGWQRZV6tPKTDAYCgSAZGBY3QDmPGT5BeFcg5Ag_nYYIIifKP-kv10v_N-TOcT3NeGBOUlAZ-9m7iT7Rk3vC--SDZdA\
U5turoBFiiPL2XXfAjM7P0r7J91gfXc0FaD6I2jDxOmym5h7Yn5phLsbC2NlIXkZp54dKHICenPl4ve6ndDIJacVeS5\
f3LEddAPV8cAFza4DjA8pZJLFrMyRvMXcL_PjKF8qPVzqVWh03lfJ4clOIxR2gOuWIc902Y5E";

let token = super::decode_token(jwt).unwrap();
assert_eq!(token.sub, "e25d37f3-b603-40de-84ba-af96012f5a42");
assert_eq!(token.email.as_deref(), Some("[email protected]"));
assert_eq!(token.organization.as_deref(), None);
assert_eq!(token.scope[0], "api");
assert_eq!(token.scope[1], "offline_access");
}
}

#[cfg(test)]
pub async fn start_mock(mocks: Vec<wiremock::Mock>) -> (wiremock::MockServer, crate::Client) {
let server = wiremock::MockServer::start().await;
Expand Down

0 comments on commit eefc58b

Please sign in to comment.