Skip to content

Commit

Permalink
JWT Refresh Token
Browse files Browse the repository at this point in the history
  • Loading branch information
Timshel committed Apr 29, 2024
1 parent 0fe93ed commit c48b2e2
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 111 deletions.
100 changes: 49 additions & 51 deletions src/api/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use num_traits::FromPrimitive;
use rocket::serde::json::Json;
use rocket::{
form::{Form, FromForm},
http::Status,
Route,
};
use serde_json::Value;
Expand All @@ -17,7 +18,8 @@ use crate::{
push::register_push_device,
ApiResult, EmptyResult, JsonResult, JsonUpcase,
},
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp},
auth,
auth::{generate_organization_api_key_login_claims, AuthMethod, AuthMethodScope, ClientHeaders, ClientIp},
db::{models::*, DbConn},
error::MapResult,
mail, util, CONFIG,
Expand Down Expand Up @@ -96,43 +98,43 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:

async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
// Extract token
let token = data.refresh_token.unwrap();

// Get device by refresh token
let mut device = Device::find_by_refresh_token(&token, conn).await.map_res("Invalid refresh token")?;

let scope = "api offline_access";
let scope_vec = vec!["api".into(), "offline_access".into()];
let refresh_token = match data.refresh_token {
Some(token) => token,
None => err_code!("Missing refresh_token", Status::Unauthorized.code),
};

// Common
let user = User::find_by_uuid(&device.user_uuid, conn).await.unwrap();
// ---
// Disabled this variable, it was used to generate the JWT
// Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
device.save(conn).await?;

let result = json!({
"access_token": access_token,
"expires_in": expires_in,
"token_type": "Bearer",
"refresh_token": device.refresh_token,
"Key": user.akey,
"PrivateKey": user.private_key,

"Kdf": user.client_kdf_type,
"KdfIterations": user.client_kdf_iter,
"KdfMemory": user.client_kdf_memory,
"KdfParallelism": user.client_kdf_parallelism,
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
"scope": scope,
"unofficialServer": true,
});

Ok(Json(result))
match auth::refresh_tokens(&refresh_token, conn).await {
Err(err) => err_code!(err.to_string(), Status::Unauthorized.code),
Ok((mut device, user, auth_tokens)) => {
// Save to update `device.updated_at` to track usage
device.save(conn).await?;

let result = json!({
"refresh_token": auth_tokens.refresh_token(),
"access_token": auth_tokens.access_token(),
"expires_in": auth_tokens.expires_in(),
"token_type": "Bearer",
"Key": user.akey,
"PrivateKey": user.private_key,

"Kdf": user.client_kdf_type,
"KdfIterations": user.client_kdf_iter,
"KdfMemory": user.client_kdf_memory,
"KdfParallelism": user.client_kdf_parallelism,
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
"scope": auth_tokens.scope(),
"unofficialServer": true,
});

Ok(Json(result))
}
}
}

async fn _password_login(
Expand All @@ -142,11 +144,7 @@ async fn _password_login(
ip: &ClientIp,
) -> JsonResult {
// Validate scope
let scope = data.scope.as_ref().unwrap();
if scope != "api offline_access" {
err!("Scope not supported")
}
let scope_vec = vec!["api".into(), "offline_access".into()];
AuthMethod::Password.check_scope(data.scope.as_ref())?;

// Ratelimit the login
crate::ratelimit::check_limit_login(&ip.ip)?;
Expand Down Expand Up @@ -279,14 +277,14 @@ async fn _password_login(
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password);
device.save(conn).await?;

let mut result = json!({
"access_token": access_token,
"expires_in": expires_in,
"access_token": auth_tokens.access_token(),
"expires_in": auth_tokens.expires_in(),
"token_type": "Bearer",
"refresh_token": device.refresh_token,
"refresh_token": auth_tokens.refresh_token(),
"Key": user.akey,
"PrivateKey": user.private_key,
//"TwoFactorToken": "11122233333444555666777888999"
Expand All @@ -301,7 +299,7 @@ async fn _password_login(
"object": "masterPasswordPolicy",
},

"scope": scope,
"scope": auth_tokens.scope(),
"unofficialServer": true,
"UserDecryptionOptions": {
"HasMasterPassword": !user.password_hash.is_empty(),
Expand All @@ -327,9 +325,9 @@ async fn _api_key_login(
crate::ratelimit::check_limit_login(&ip.ip)?;

// Validate scope
match data.scope.as_ref().unwrap().as_ref() {
"api" => _user_api_key_login(data, user_uuid, conn, ip).await,
"api.organization" => _organization_api_key_login(data, conn, ip).await,
match data.scope.as_ref() {
Some(scope) if scope == &AuthMethod::UserApiKey.scope() => _user_api_key_login(data, user_uuid, conn, ip).await,
Some(scope) if scope == &AuthMethod::OrgApiKey.scope() => _organization_api_key_login(data, conn, ip).await,
_ => err!("Scope not supported"),
}
}
Expand Down Expand Up @@ -395,24 +393,24 @@ async fn _user_api_key_login(
}
}

// Common
let scope_vec = vec!["api".into()];
// ---
// Disabled this variable, it was used to generate the JWT
// Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
let access_claims = auth::LoginJwtClaims::default(&device, &user, &auth::AuthMethod::UserApiKey);

// Save to update `device.updated_at` to track usage
device.save(conn).await?;

info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);

// Note: No refresh_token is returned. The CLI just repeats the
// client_credentials login flow when the existing token expires.
let result = json!({
"access_token": access_token,
"expires_in": expires_in,
"access_token": access_claims.token(),
"expires_in": access_claims.expires_in(),
"token_type": "Bearer",
"Key": user.akey,
"PrivateKey": user.private_key,
Expand All @@ -422,7 +420,7 @@ async fn _user_api_key_login(
"KdfMemory": user.client_kdf_memory,
"KdfParallelism": user.client_kdf_parallelism,
"ResetMasterPassword": false, // TODO: Same as above
"scope": "api",
"scope": auth::AuthMethod::UserApiKey.scope(),
"unofficialServer": true,
});

Expand Down Expand Up @@ -454,7 +452,7 @@ async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &
"access_token": access_token,
"expires_in": 3600,
"token_type": "Bearer",
"scope": "api.organization",
"scope": auth::AuthMethod::OrgApiKey.scope(),
"unofficialServer": true,
})))
}
Expand Down
2 changes: 1 addition & 1 deletion src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use crate::db::{models::User, DbConn};
use crate::util;

// Type aliases for API methods results
type ApiResult<T> = Result<T, crate::error::Error>;
pub type ApiResult<T> = Result<T, crate::error::Error>;
pub type JsonResult = ApiResult<Json<Value>>;
pub type EmptyResult = ApiResult<()>;

Expand Down

0 comments on commit c48b2e2

Please sign in to comment.