Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-11-16 04:12:53 +01:00
JWT Refresh Token
Dieser Commit ist enthalten in:
Ursprung
0fe93edea6
Commit
c48b2e241a
4 geänderte Dateien mit 248 neuen und 108 gelöschten Zeilen
|
@ -3,6 +3,7 @@ use num_traits::FromPrimitive;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::{Form, FromForm},
|
form::{Form, FromForm},
|
||||||
|
http::Status,
|
||||||
Route,
|
Route,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
@ -17,7 +18,8 @@ use crate::{
|
||||||
push::register_push_device,
|
push::register_push_device,
|
||||||
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
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},
|
db::{models::*, DbConn},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
mail, util, CONFIG,
|
mail, util, CONFIG,
|
||||||
|
@ -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 {
|
async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
||||||
// Extract token
|
// Extract token
|
||||||
let token = data.refresh_token.unwrap();
|
let refresh_token = match data.refresh_token {
|
||||||
|
Some(token) => token,
|
||||||
|
None => err_code!("Missing refresh_token", Status::Unauthorized.code),
|
||||||
|
};
|
||||||
|
|
||||||
// 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()];
|
|
||||||
|
|
||||||
// Common
|
|
||||||
let user = User::find_by_uuid(&device.user_uuid, conn).await.unwrap();
|
|
||||||
// ---
|
// ---
|
||||||
// Disabled this variable, it was used to generate the JWT
|
// 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
|
// 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
|
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||||
// ---
|
// ---
|
||||||
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
|
match auth::refresh_tokens(&refresh_token, conn).await {
|
||||||
device.save(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!({
|
let result = json!({
|
||||||
"access_token": access_token,
|
"refresh_token": auth_tokens.refresh_token(),
|
||||||
"expires_in": expires_in,
|
"access_token": auth_tokens.access_token(),
|
||||||
"token_type": "Bearer",
|
"expires_in": auth_tokens.expires_in(),
|
||||||
"refresh_token": device.refresh_token,
|
"token_type": "Bearer",
|
||||||
"Key": user.akey,
|
"Key": user.akey,
|
||||||
"PrivateKey": user.private_key,
|
"PrivateKey": user.private_key,
|
||||||
|
|
||||||
"Kdf": user.client_kdf_type,
|
"Kdf": user.client_kdf_type,
|
||||||
"KdfIterations": user.client_kdf_iter,
|
"KdfIterations": user.client_kdf_iter,
|
||||||
"KdfMemory": user.client_kdf_memory,
|
"KdfMemory": user.client_kdf_memory,
|
||||||
"KdfParallelism": user.client_kdf_parallelism,
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
||||||
"scope": scope,
|
"scope": auth_tokens.scope(),
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _password_login(
|
async fn _password_login(
|
||||||
|
@ -142,11 +144,7 @@ async fn _password_login(
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
let scope = data.scope.as_ref().unwrap();
|
AuthMethod::Password.check_scope(data.scope.as_ref())?;
|
||||||
if scope != "api offline_access" {
|
|
||||||
err!("Scope not supported")
|
|
||||||
}
|
|
||||||
let scope_vec = vec!["api".into(), "offline_access".into()];
|
|
||||||
|
|
||||||
// Ratelimit the login
|
// Ratelimit the login
|
||||||
crate::ratelimit::check_limit_login(&ip.ip)?;
|
crate::ratelimit::check_limit_login(&ip.ip)?;
|
||||||
|
@ -279,14 +277,14 @@ async fn _password_login(
|
||||||
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||||
// ---
|
// ---
|
||||||
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
// 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?;
|
device.save(conn).await?;
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
"access_token": access_token,
|
"access_token": auth_tokens.access_token(),
|
||||||
"expires_in": expires_in,
|
"expires_in": auth_tokens.expires_in(),
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"refresh_token": device.refresh_token,
|
"refresh_token": auth_tokens.refresh_token(),
|
||||||
"Key": user.akey,
|
"Key": user.akey,
|
||||||
"PrivateKey": user.private_key,
|
"PrivateKey": user.private_key,
|
||||||
//"TwoFactorToken": "11122233333444555666777888999"
|
//"TwoFactorToken": "11122233333444555666777888999"
|
||||||
|
@ -301,7 +299,7 @@ async fn _password_login(
|
||||||
"object": "masterPasswordPolicy",
|
"object": "masterPasswordPolicy",
|
||||||
},
|
},
|
||||||
|
|
||||||
"scope": scope,
|
"scope": auth_tokens.scope(),
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
"UserDecryptionOptions": {
|
"UserDecryptionOptions": {
|
||||||
"HasMasterPassword": !user.password_hash.is_empty(),
|
"HasMasterPassword": !user.password_hash.is_empty(),
|
||||||
|
@ -327,9 +325,9 @@ async fn _api_key_login(
|
||||||
crate::ratelimit::check_limit_login(&ip.ip)?;
|
crate::ratelimit::check_limit_login(&ip.ip)?;
|
||||||
|
|
||||||
// Validate scope
|
// Validate scope
|
||||||
match data.scope.as_ref().unwrap().as_ref() {
|
match data.scope.as_ref() {
|
||||||
"api" => _user_api_key_login(data, user_uuid, conn, ip).await,
|
Some(scope) if scope == &AuthMethod::UserApiKey.scope() => _user_api_key_login(data, user_uuid, conn, ip).await,
|
||||||
"api.organization" => _organization_api_key_login(data, conn, ip).await,
|
Some(scope) if scope == &AuthMethod::OrgApiKey.scope() => _organization_api_key_login(data, conn, ip).await,
|
||||||
_ => err!("Scope not supported"),
|
_ => err!("Scope not supported"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -395,15 +393,15 @@ async fn _user_api_key_login(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common
|
|
||||||
let scope_vec = vec!["api".into()];
|
|
||||||
// ---
|
// ---
|
||||||
// Disabled this variable, it was used to generate the JWT
|
// 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
|
// 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
|
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||||
// ---
|
// ---
|
||||||
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
// 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?;
|
device.save(conn).await?;
|
||||||
|
|
||||||
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
|
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
|
||||||
|
@ -411,8 +409,8 @@ async fn _user_api_key_login(
|
||||||
// Note: No refresh_token is returned. The CLI just repeats the
|
// Note: No refresh_token is returned. The CLI just repeats the
|
||||||
// client_credentials login flow when the existing token expires.
|
// client_credentials login flow when the existing token expires.
|
||||||
let result = json!({
|
let result = json!({
|
||||||
"access_token": access_token,
|
"access_token": access_claims.token(),
|
||||||
"expires_in": expires_in,
|
"expires_in": access_claims.expires_in(),
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"Key": user.akey,
|
"Key": user.akey,
|
||||||
"PrivateKey": user.private_key,
|
"PrivateKey": user.private_key,
|
||||||
|
@ -422,7 +420,7 @@ async fn _user_api_key_login(
|
||||||
"KdfMemory": user.client_kdf_memory,
|
"KdfMemory": user.client_kdf_memory,
|
||||||
"KdfParallelism": user.client_kdf_parallelism,
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false, // TODO: Same as above
|
"ResetMasterPassword": false, // TODO: Same as above
|
||||||
"scope": "api",
|
"scope": auth::AuthMethod::UserApiKey.scope(),
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -454,7 +452,7 @@ async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"expires_in": 3600,
|
"expires_in": 3600,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"scope": "api.organization",
|
"scope": auth::AuthMethod::OrgApiKey.scope(),
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ use crate::db::{models::User, DbConn};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
// Type aliases for API methods results
|
// 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 JsonResult = ApiResult<Json<Value>>;
|
||||||
pub type EmptyResult = ApiResult<()>;
|
pub type EmptyResult = ApiResult<()>;
|
||||||
|
|
||||||
|
|
199
src/auth.rs
199
src/auth.rs
|
@ -9,11 +9,13 @@ use openssl::rsa::Rsa;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::ser::Serialize;
|
use serde::ser::Serialize;
|
||||||
|
|
||||||
|
use crate::api::ApiResult;
|
||||||
use crate::{error::Error, CONFIG};
|
use crate::{error::Error, CONFIG};
|
||||||
|
|
||||||
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
||||||
|
|
||||||
pub static DEFAULT_VALIDITY: Lazy<TimeDelta> = Lazy::new(|| TimeDelta::try_hours(2).unwrap());
|
pub static DEFAULT_REFRESH_VALIDITY: Lazy<TimeDelta> = Lazy::new(|| TimeDelta::try_days(30).unwrap());
|
||||||
|
pub static DEFAULT_ACCESS_VALIDITY: Lazy<TimeDelta> = Lazy::new(|| TimeDelta::try_hours(2).unwrap());
|
||||||
static JWT_HEADER: Lazy<Header> = Lazy::new(|| Header::new(JWT_ALGORITHM));
|
static JWT_HEADER: Lazy<Header> = Lazy::new(|| Header::new(JWT_ALGORITHM));
|
||||||
|
|
||||||
pub static JWT_LOGIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|login", CONFIG.domain_origin()));
|
pub static JWT_LOGIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|login", CONFIG.domain_origin()));
|
||||||
|
@ -91,6 +93,10 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn decode_refresh(token: &str) -> Result<RefreshJwtClaims, Error> {
|
||||||
|
decode_jwt(token, JWT_LOGIN_ISSUER.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn decode_login(token: &str) -> Result<LoginJwtClaims, Error> {
|
pub fn decode_login(token: &str) -> Result<LoginJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_LOGIN_ISSUER.to_string())
|
decode_jwt(token, JWT_LOGIN_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
@ -164,6 +170,68 @@ pub struct LoginJwtClaims {
|
||||||
pub amr: Vec<String>,
|
pub amr: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LoginJwtClaims {
|
||||||
|
pub fn new(device: &Device, user: &User, nbf: i64, exp: i64, scope: Vec<String>) -> Self {
|
||||||
|
// ---
|
||||||
|
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
|
||||||
|
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
|
||||||
|
// Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
|
||||||
|
// ---
|
||||||
|
// fn arg: orgs: Vec<super::UserOrganization>,
|
||||||
|
// ---
|
||||||
|
// let orgowner: Vec<_> = orgs.iter().filter(|o| o.atype == 0).map(|o| o.org_uuid.clone()).collect();
|
||||||
|
// let orgadmin: Vec<_> = orgs.iter().filter(|o| o.atype == 1).map(|o| o.org_uuid.clone()).collect();
|
||||||
|
// let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();
|
||||||
|
// let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
||||||
|
|
||||||
|
// Create the JWT claims struct, to send to the client
|
||||||
|
Self {
|
||||||
|
nbf,
|
||||||
|
exp,
|
||||||
|
iss: JWT_LOGIN_ISSUER.to_string(),
|
||||||
|
sub: user.uuid.clone(),
|
||||||
|
premium: true,
|
||||||
|
name: user.name.clone(),
|
||||||
|
email: user.email.clone(),
|
||||||
|
email_verified: !CONFIG.mail_enabled() || user.verified_at.is_some(),
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
|
||||||
|
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
|
||||||
|
// Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
|
||||||
|
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||||
|
// ---
|
||||||
|
// orgowner,
|
||||||
|
// orgadmin,
|
||||||
|
// orguser,
|
||||||
|
// orgmanager,
|
||||||
|
sstamp: user.security_stamp.clone(),
|
||||||
|
device: device.uuid.clone(),
|
||||||
|
scope,
|
||||||
|
amr: vec!["Application".into()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default(device: &Device, user: &User, auth_method: &AuthMethod) -> Self {
|
||||||
|
let time_now = Utc::now();
|
||||||
|
Self::new(
|
||||||
|
device,
|
||||||
|
user,
|
||||||
|
time_now.timestamp(),
|
||||||
|
(time_now + *DEFAULT_ACCESS_VALIDITY).timestamp(),
|
||||||
|
auth_method.scope_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(&self) -> String {
|
||||||
|
encode_jwt(&self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expires_in(&self) -> i64 {
|
||||||
|
self.exp - Utc::now().timestamp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct InviteJwtClaims {
|
pub struct InviteJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
|
@ -876,3 +944,132 @@ impl<'r> FromRequest<'r> for WsAccessTokenHeader {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum AuthMethod {
|
||||||
|
OrgApiKey,
|
||||||
|
Password,
|
||||||
|
UserApiKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AuthMethodScope {
|
||||||
|
fn scope_vec(&self) -> Vec<String>;
|
||||||
|
fn scope(&self) -> String;
|
||||||
|
fn check_scope(&self, scope: Option<&String>) -> ApiResult<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthMethodScope for AuthMethod {
|
||||||
|
fn scope(&self) -> String {
|
||||||
|
match self {
|
||||||
|
AuthMethod::OrgApiKey => "api.organization".to_string(),
|
||||||
|
AuthMethod::Password => "api offline_access".to_string(),
|
||||||
|
AuthMethod::UserApiKey => "api".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scope_vec(&self) -> Vec<String> {
|
||||||
|
self.scope().split_whitespace().map(str::to_string).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_scope(&self, scope: Option<&String>) -> ApiResult<String> {
|
||||||
|
let method_scope = self.scope();
|
||||||
|
match scope {
|
||||||
|
None => err!("Missing scope"),
|
||||||
|
Some(scope) if scope == &method_scope => Ok(method_scope),
|
||||||
|
Some(scope) => err!(format!("Scope ({scope}) not supported")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RefreshJwtClaims {
|
||||||
|
// Not before
|
||||||
|
pub nbf: i64,
|
||||||
|
// Expiration time
|
||||||
|
pub exp: i64,
|
||||||
|
// Issuer
|
||||||
|
pub iss: String,
|
||||||
|
// Subject
|
||||||
|
pub sub: AuthMethod,
|
||||||
|
|
||||||
|
pub device_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthTokens {
|
||||||
|
pub refresh_claims: RefreshJwtClaims,
|
||||||
|
pub access_claims: LoginJwtClaims,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthTokens {
|
||||||
|
pub fn refresh_token(&self) -> String {
|
||||||
|
encode_jwt(&self.refresh_claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn access_token(&self) -> String {
|
||||||
|
self.access_claims.token()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expires_in(&self) -> i64 {
|
||||||
|
self.access_claims.expires_in()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scope(&self) -> String {
|
||||||
|
self.refresh_claims.sub.scope()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create refresh_token and access_token with default validity
|
||||||
|
pub fn new(device: &Device, user: &User, sub: AuthMethod) -> Self {
|
||||||
|
let time_now = Utc::now();
|
||||||
|
|
||||||
|
let access_claims = LoginJwtClaims::default(device, user, &sub);
|
||||||
|
|
||||||
|
let refresh_claims = RefreshJwtClaims {
|
||||||
|
nbf: time_now.timestamp(),
|
||||||
|
exp: (time_now + *DEFAULT_REFRESH_VALIDITY).timestamp(),
|
||||||
|
iss: JWT_LOGIN_ISSUER.to_string(),
|
||||||
|
sub,
|
||||||
|
device_token: device.refresh_token.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
refresh_claims,
|
||||||
|
access_claims,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_tokens(refresh_token: &str, conn: &mut DbConn) -> ApiResult<(Device, User, AuthTokens)> {
|
||||||
|
let time_now = Utc::now();
|
||||||
|
|
||||||
|
let refresh_claims = match decode_refresh(refresh_token) {
|
||||||
|
Err(err) => err!(format!("Impossible to read refresh_token: {err}")),
|
||||||
|
Ok(claims) => claims,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get device by refresh token
|
||||||
|
let mut device = match Device::find_by_refresh_token(&refresh_claims.device_token, conn).await {
|
||||||
|
None => err!("Invalid refresh token"),
|
||||||
|
Some(device) => device,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to update `updated_at`.
|
||||||
|
device.save(conn).await?;
|
||||||
|
|
||||||
|
let user = match User::find_by_uuid(&device.user_uuid, conn).await {
|
||||||
|
None => err!("Impossible to find user"),
|
||||||
|
Some(user) => user,
|
||||||
|
};
|
||||||
|
|
||||||
|
if refresh_claims.exp < time_now.timestamp() {
|
||||||
|
err!("Expired refresh token");
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_tokens = match refresh_claims.sub {
|
||||||
|
AuthMethod::Password => AuthTokens::new(&device, &user, refresh_claims.sub),
|
||||||
|
_ => err!("Invalid auth method cannot refresh token"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((device, user, auth_tokens))
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
use data_encoding::{BASE64, BASE64URL};
|
||||||
|
|
||||||
use crate::{crypto, CONFIG};
|
use crate::crypto;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
|
@ -42,13 +43,12 @@ impl Device {
|
||||||
|
|
||||||
push_uuid: None,
|
push_uuid: None,
|
||||||
push_token: None,
|
push_token: None,
|
||||||
refresh_token: String::new(),
|
refresh_token: crypto::encode_random_bytes::<64>(BASE64URL),
|
||||||
twofactor_remember: None,
|
twofactor_remember: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||||
use data_encoding::BASE64;
|
|
||||||
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
||||||
self.twofactor_remember = Some(twofactor_remember.clone());
|
self.twofactor_remember = Some(twofactor_remember.clone());
|
||||||
|
|
||||||
|
@ -59,61 +59,6 @@ impl Device {
|
||||||
self.twofactor_remember = None;
|
self.twofactor_remember = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_tokens(&mut self, user: &super::User, scope: Vec<String>) -> (String, i64) {
|
|
||||||
// If there is no refresh token, we create one
|
|
||||||
if self.refresh_token.is_empty() {
|
|
||||||
use data_encoding::BASE64URL;
|
|
||||||
self.refresh_token = crypto::encode_random_bytes::<64>(BASE64URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the expiration of the device and the last update date
|
|
||||||
let time_now = Utc::now();
|
|
||||||
self.updated_at = time_now.naive_utc();
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
|
|
||||||
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
|
|
||||||
// Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
|
|
||||||
// ---
|
|
||||||
// fn arg: orgs: Vec<super::UserOrganization>,
|
|
||||||
// ---
|
|
||||||
// let orgowner: Vec<_> = orgs.iter().filter(|o| o.atype == 0).map(|o| o.org_uuid.clone()).collect();
|
|
||||||
// let orgadmin: Vec<_> = orgs.iter().filter(|o| o.atype == 1).map(|o| o.org_uuid.clone()).collect();
|
|
||||||
// let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();
|
|
||||||
// let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
|
||||||
|
|
||||||
// Create the JWT claims struct, to send to the client
|
|
||||||
use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
|
|
||||||
let claims = LoginJwtClaims {
|
|
||||||
nbf: time_now.timestamp(),
|
|
||||||
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
|
|
||||||
iss: JWT_LOGIN_ISSUER.to_string(),
|
|
||||||
sub: user.uuid.clone(),
|
|
||||||
|
|
||||||
premium: true,
|
|
||||||
name: user.name.clone(),
|
|
||||||
email: user.email.clone(),
|
|
||||||
email_verified: !CONFIG.mail_enabled() || user.verified_at.is_some(),
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
|
|
||||||
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
|
|
||||||
// Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
|
|
||||||
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
|
||||||
// ---
|
|
||||||
// orgowner,
|
|
||||||
// orgadmin,
|
|
||||||
// orguser,
|
|
||||||
// orgmanager,
|
|
||||||
sstamp: user.security_stamp.clone(),
|
|
||||||
device: self.uuid.clone(),
|
|
||||||
scope,
|
|
||||||
amr: vec!["Application".into()],
|
|
||||||
};
|
|
||||||
|
|
||||||
(encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_push_device(&self) -> bool {
|
pub fn is_push_device(&self) -> bool {
|
||||||
matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios)
|
matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios)
|
||||||
}
|
}
|
||||||
|
|
Laden …
In neuem Issue referenzieren