mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
Implement Nextcloud login flow
This commit is contained in:
23
Cargo.lock
generated
23
Cargo.lock
generated
@@ -934,6 +934,20 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashmap"
|
||||||
|
version = "6.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"lock_api",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.9"
|
version = "0.7.9"
|
||||||
@@ -1312,6 +1326,12 @@ version = "0.12.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
@@ -2745,6 +2765,7 @@ dependencies = [
|
|||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"clap",
|
"clap",
|
||||||
|
"dashmap",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"opentelemetry-otlp",
|
"opentelemetry-otlp",
|
||||||
"opentelemetry-semantic-conventions",
|
"opentelemetry-semantic-conventions",
|
||||||
@@ -2769,6 +2790,7 @@ dependencies = [
|
|||||||
"tracing-actix-web",
|
"tracing-actix-web",
|
||||||
"tracing-opentelemetry",
|
"tracing-opentelemetry",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2876,6 +2898,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"password-auth",
|
"password-auth",
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"rstest",
|
"rstest",
|
||||||
"rstest_reuse",
|
"rstest_reuse",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ reqwest = { version = "0.12", features = [
|
|||||||
"charset",
|
"charset",
|
||||||
"http2",
|
"http2",
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
|
dashmap = "6.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_store = { workspace = true }
|
rustical_store = { workspace = true }
|
||||||
@@ -131,6 +132,8 @@ clap = { version = "4.5", features = ["derive", "env"] }
|
|||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
tracing-actix-web = { workspace = true }
|
tracing-actix-web = { workspace = true }
|
||||||
|
uuid.workspace = true
|
||||||
|
dashmap.workspace = true
|
||||||
|
|
||||||
opentelemetry = { version = "0.27", optional = true }
|
opentelemetry = { version = "0.27", optional = true }
|
||||||
opentelemetry-otlp = { version = "0.27", optional = true }
|
opentelemetry-otlp = { version = "0.27", optional = true }
|
||||||
@@ -139,7 +142,6 @@ opentelemetry_sdk = { version = "0.27", features = [
|
|||||||
], optional = true }
|
], optional = true }
|
||||||
opentelemetry-semantic-conventions = { version = "0.27", optional = true }
|
opentelemetry-semantic-conventions = { version = "0.27", optional = true }
|
||||||
tracing-opentelemetry = { version = "0.28", optional = true }
|
tracing-opentelemetry = { version = "0.28", optional = true }
|
||||||
|
|
||||||
tracing-subscriber = { version = "0.3", features = [
|
tracing-subscriber = { version = "0.3", features = [
|
||||||
"env-filter",
|
"env-filter",
|
||||||
"fmt",
|
"fmt",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ a CalDAV/CardDAV server
|
|||||||
- lightweight (the container image contains only one binary)
|
- lightweight (the container image contains only one binary)
|
||||||
- adequately fast (I'd say blazingly fast™ :fire: if I did the benchmarks to back that claim up)
|
- adequately fast (I'd say blazingly fast™ :fire: if I did the benchmarks to back that claim up)
|
||||||
- deleted calendars are recoverable
|
- deleted calendars are recoverable
|
||||||
|
- Nextcloud login flow (In DAVx5 you can login through the Nextcloud flow and automatically generate an app token)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ derive_more = { workspace = true }
|
|||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = { workspace = true }
|
rstest = { workspace = true }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use async_trait::async_trait;
|
|||||||
pub trait AuthenticationProvider: 'static {
|
pub trait AuthenticationProvider: 'static {
|
||||||
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>;
|
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>;
|
||||||
async fn validate_user_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error>;
|
async fn validate_user_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error>;
|
||||||
|
async fn add_app_token(&self, user_id: &str, name: String, token: String) -> Result<(), Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use middleware::AuthenticationMiddleware;
|
pub use middleware::AuthenticationMiddleware;
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
use super::AuthenticationProvider;
|
use super::{user::AppToken, AuthenticationProvider};
|
||||||
use crate::{auth::User, error::Error};
|
use crate::{auth::User, error::Error};
|
||||||
|
use anyhow::anyhow;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use password_hash::PasswordHasher;
|
||||||
|
use pbkdf2::{
|
||||||
|
password_hash::{self, rand_core::OsRng, SaltString},
|
||||||
|
Params,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, fs, io};
|
use std::{collections::HashMap, fs, io, ops::Deref};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
@@ -18,6 +24,7 @@ pub struct TomlUserStoreConfig {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TomlPrincipalStore {
|
pub struct TomlPrincipalStore {
|
||||||
pub principals: RwLock<HashMap<String, User>>,
|
pub principals: RwLock<HashMap<String, User>>,
|
||||||
|
config: TomlUserStoreConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@@ -35,8 +42,21 @@ impl TomlPrincipalStore {
|
|||||||
principals: RwLock::new(HashMap::from_iter(
|
principals: RwLock::new(HashMap::from_iter(
|
||||||
principals.into_iter().map(|user| (user.id.clone(), user)),
|
principals.into_iter().map(|user| (user.id.clone(), user)),
|
||||||
)),
|
)),
|
||||||
|
config,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save(&self, principals: &HashMap<String, User>) -> Result<(), Error> {
|
||||||
|
let out = toml::to_string_pretty(&TomlDataModel {
|
||||||
|
principals: principals
|
||||||
|
.iter()
|
||||||
|
.map(|(_, value)| value.to_owned())
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
.map_err(|_| anyhow!("Error saving principal database"))?;
|
||||||
|
fs::write(&self.config.path, out)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -70,4 +90,32 @@ impl AuthenticationProvider for TomlPrincipalStore {
|
|||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn add_app_token(&self, user_id: &str, name: String, token: String) -> Result<(), Error> {
|
||||||
|
let mut principals = self.principals.write().await;
|
||||||
|
if let Some(principal) = principals.get_mut(user_id) {
|
||||||
|
let salt = SaltString::generate(OsRng);
|
||||||
|
let token_hash = pbkdf2::Pbkdf2
|
||||||
|
.hash_password_customized(
|
||||||
|
token.as_bytes(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Params {
|
||||||
|
rounds: 1000,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&salt,
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::PasswordHash)?
|
||||||
|
.to_string();
|
||||||
|
principal.app_tokens.push(AppToken {
|
||||||
|
name,
|
||||||
|
token: token_hash,
|
||||||
|
});
|
||||||
|
self.save(principals.deref())?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use actix_web::{http::StatusCode, ResponseError};
|
use actix_web::{http::StatusCode, ResponseError};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Not found")]
|
#[error("Not found")]
|
||||||
NotFound,
|
NotFound,
|
||||||
@@ -15,6 +14,12 @@ pub enum Error {
|
|||||||
#[error("Read-only")]
|
#[error("Read-only")]
|
||||||
ReadOnly,
|
ReadOnly,
|
||||||
|
|
||||||
|
#[error("Error generating password hash")]
|
||||||
|
PasswordHash,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
ParserError(#[from] ical::parser::ParserError),
|
ParserError(#[from] ical::parser::ParserError),
|
||||||
|
|
||||||
|
|||||||
10
src/app.rs
10
src/app.rs
@@ -10,12 +10,17 @@ use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing_actix_web::TracingLogger;
|
use tracing_actix_web::TracingLogger;
|
||||||
|
|
||||||
|
use crate::config::NextcloudLoginConfig;
|
||||||
|
use crate::nextcloud_login::{configure_nextcloud_login, NextcloudFlows};
|
||||||
|
|
||||||
pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
||||||
addr_store: Arc<AS>,
|
addr_store: Arc<AS>,
|
||||||
cal_store: Arc<CS>,
|
cal_store: Arc<CS>,
|
||||||
subscription_store: Arc<S>,
|
subscription_store: Arc<S>,
|
||||||
auth_provider: Arc<impl AuthenticationProvider>,
|
auth_provider: Arc<impl AuthenticationProvider>,
|
||||||
frontend_config: FrontendConfig,
|
frontend_config: FrontendConfig,
|
||||||
|
nextcloud_login_config: NextcloudLoginConfig,
|
||||||
|
nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||||
) -> App<
|
) -> App<
|
||||||
impl ServiceFactory<
|
impl ServiceFactory<
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
@@ -59,5 +64,10 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
|||||||
}))
|
}))
|
||||||
.service(web::redirect("/", "/frontend").see_other());
|
.service(web::redirect("/", "/frontend").see_other());
|
||||||
}
|
}
|
||||||
|
if nextcloud_login_config.enabled {
|
||||||
|
app = app.configure(|cfg| {
|
||||||
|
configure_nextcloud_login(cfg, nextcloud_flows_state, auth_provider.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
dav_push: DavPushConfig::default(),
|
dav_push: DavPushConfig::default(),
|
||||||
|
nextcloud_login: Default::default(),
|
||||||
};
|
};
|
||||||
let generated_config = toml::to_string(&config)?;
|
let generated_config = toml::to_string(&config)?;
|
||||||
println!("{generated_config}");
|
println!("{generated_config}");
|
||||||
|
|||||||
@@ -63,6 +63,18 @@ impl Default for DavPushConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields, default)]
|
||||||
|
pub struct NextcloudLoginConfig {
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NextcloudLoginConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { enabled: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -75,4 +87,6 @@ pub struct Config {
|
|||||||
pub tracing: TracingConfig,
|
pub tracing: TracingConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dav_push: DavPushConfig,
|
pub dav_push: DavPushConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub nextcloud_login: NextcloudLoginConfig,
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/main.rs
23
src/main.rs
@@ -6,6 +6,7 @@ use app::make_app;
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use commands::{cmd_gen_config, cmd_pwhash};
|
use commands::{cmd_gen_config, cmd_pwhash};
|
||||||
use config::{DataStoreConfig, SqliteDataStoreConfig};
|
use config::{DataStoreConfig, SqliteDataStoreConfig};
|
||||||
|
use nextcloud_login::NextcloudFlows;
|
||||||
use rustical_dav::push::push_notifier;
|
use rustical_dav::push::push_notifier;
|
||||||
use rustical_store::auth::TomlPrincipalStore;
|
use rustical_store::auth::TomlPrincipalStore;
|
||||||
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
|
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
|
||||||
@@ -20,6 +21,7 @@ use tokio::sync::mpsc::Receiver;
|
|||||||
mod app;
|
mod app;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod nextcloud_login;
|
||||||
mod setup_tracing;
|
mod setup_tracing;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -94,6 +96,8 @@ async fn main() -> Result<()> {
|
|||||||
config::AuthConfig::Toml(config) => Arc::new(TomlPrincipalStore::new(config)?),
|
config::AuthConfig::Toml(config) => Arc::new(TomlPrincipalStore::new(config)?),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let nextcloud_flows = Arc::new(NextcloudFlows::default());
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
make_app(
|
make_app(
|
||||||
addr_store.clone(),
|
addr_store.clone(),
|
||||||
@@ -101,6 +105,8 @@ async fn main() -> Result<()> {
|
|||||||
subscription_store.clone(),
|
subscription_store.clone(),
|
||||||
user_store.clone(),
|
user_store.clone(),
|
||||||
config.frontend.clone(),
|
config.frontend.clone(),
|
||||||
|
config.nextcloud_login.clone(),
|
||||||
|
nextcloud_flows.clone(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.bind((config.http.host, config.http.port))?
|
.bind((config.http.host, config.http.port))?
|
||||||
@@ -117,8 +123,12 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{app::make_app, commands::generate_frontend_secret, get_data_stores};
|
use crate::{
|
||||||
|
app::make_app, commands::generate_frontend_secret, config::NextcloudLoginConfig,
|
||||||
|
get_data_stores, nextcloud_login::NextcloudFlows,
|
||||||
|
};
|
||||||
use actix_web::{http::StatusCode, test::TestRequest};
|
use actix_web::{http::StatusCode, test::TestRequest};
|
||||||
|
use anyhow::anyhow;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustical_frontend::FrontendConfig;
|
use rustical_frontend::FrontendConfig;
|
||||||
use rustical_store::auth::AuthenticationProvider;
|
use rustical_store::auth::AuthenticationProvider;
|
||||||
@@ -143,6 +153,15 @@ mod tests {
|
|||||||
) -> Result<Option<rustical_store::auth::User>, rustical_store::Error> {
|
) -> Result<Option<rustical_store::auth::User>, rustical_store::Error> {
|
||||||
Err(rustical_store::Error::NotFound)
|
Err(rustical_store::Error::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn add_app_token(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
name: String,
|
||||||
|
token: String,
|
||||||
|
) -> Result<(), rustical_store::Error> {
|
||||||
|
Err(rustical_store::Error::Other(anyhow!("Not implemented")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -167,6 +186,8 @@ mod tests {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
secret_key: generate_frontend_secret(),
|
secret_key: generate_frontend_secret(),
|
||||||
},
|
},
|
||||||
|
NextcloudLoginConfig { enabled: false },
|
||||||
|
Arc::new(NextcloudFlows::default()),
|
||||||
);
|
);
|
||||||
let app = actix_web::test::init_service(app).await;
|
let app = actix_web::test::init_service(app).await;
|
||||||
let req = TestRequest::get().uri("/").to_request();
|
let req = TestRequest::get().uri("/").to_request();
|
||||||
|
|||||||
164
src/nextcloud_login.rs
Normal file
164
src/nextcloud_login.rs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
use actix_web::{
|
||||||
|
http::header::{self, ContentType},
|
||||||
|
web::{self, Data, Form, Json, Path, ServiceConfig},
|
||||||
|
HttpRequest, HttpResponse, Responder,
|
||||||
|
};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
|
use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider, User};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct NextcloudFlows {
|
||||||
|
tokens: DashMap<String, String>,
|
||||||
|
completed_flows: DashMap<String, NextcloudSuccessResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct NextcloudLoginPoll {
|
||||||
|
token: String,
|
||||||
|
endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct NextcloudLoginResponse {
|
||||||
|
poll: NextcloudLoginPoll,
|
||||||
|
login: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_nextcloud_login(
|
||||||
|
req: HttpRequest,
|
||||||
|
state: Data<NextcloudFlows>,
|
||||||
|
) -> Json<NextcloudLoginResponse> {
|
||||||
|
let flow_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
let token = uuid::Uuid::new_v4().to_string();
|
||||||
|
let poll_url = req
|
||||||
|
.resource_map()
|
||||||
|
.url_for(&req, "nc_login_poll", [&flow_id])
|
||||||
|
.unwrap();
|
||||||
|
let flow_url = req
|
||||||
|
.resource_map()
|
||||||
|
.url_for(&req, "nc_login_flow", [&flow_id])
|
||||||
|
.unwrap();
|
||||||
|
state.tokens.insert(flow_id, token.to_owned());
|
||||||
|
Json(NextcloudLoginResponse {
|
||||||
|
login: flow_url.to_string(),
|
||||||
|
poll: NextcloudLoginPoll {
|
||||||
|
token,
|
||||||
|
endpoint: poll_url.to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct NextcloudSuccessResponse {
|
||||||
|
server: String,
|
||||||
|
login_name: String,
|
||||||
|
app_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct NextcloudPollForm {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_nextcloud_poll<AP: AuthenticationProvider>(
|
||||||
|
state: Data<NextcloudFlows>,
|
||||||
|
form: Form<NextcloudPollForm>,
|
||||||
|
path: Path<String>,
|
||||||
|
auth_provider: Data<AP>,
|
||||||
|
req: HttpRequest,
|
||||||
|
) -> Result<HttpResponse, rustical_store::Error> {
|
||||||
|
let flow = path.into_inner();
|
||||||
|
match state.tokens.get(&flow) {
|
||||||
|
None => return Ok(HttpResponse::Unauthorized().finish()),
|
||||||
|
Some(dash_ref) if &form.token != dash_ref.value() => {
|
||||||
|
return Ok(HttpResponse::Unauthorized().finish())
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let app_name = req
|
||||||
|
.headers()
|
||||||
|
.get(header::USER_AGENT)
|
||||||
|
.map(|val| val.to_str().unwrap_or("Client"))
|
||||||
|
.unwrap_or("Client");
|
||||||
|
|
||||||
|
if let Some((_, response)) = state.completed_flows.remove(&flow) {
|
||||||
|
auth_provider
|
||||||
|
.add_app_token(
|
||||||
|
&response.login_name,
|
||||||
|
app_name.to_owned(),
|
||||||
|
response.app_password.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
state.tokens.remove(&flow);
|
||||||
|
Ok(Json(response).respond_to(&req).map_into_boxed_body())
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_app_token() -> String {
|
||||||
|
rand::thread_rng()
|
||||||
|
.sample_iter(Alphanumeric)
|
||||||
|
.map(char::from)
|
||||||
|
.take(64)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_nextcloud_flow(
|
||||||
|
user: User,
|
||||||
|
state: Data<NextcloudFlows>,
|
||||||
|
path: Path<String>,
|
||||||
|
req: HttpRequest,
|
||||||
|
) -> Result<impl Responder, rustical_store::Error> {
|
||||||
|
let flow = path.into_inner();
|
||||||
|
if !state.tokens.contains_key(&flow) {
|
||||||
|
return Ok(HttpResponse::NotFound().body("Login flow not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.completed_flows.insert(
|
||||||
|
flow,
|
||||||
|
NextcloudSuccessResponse {
|
||||||
|
server: req.full_url().origin().unicode_serialization(),
|
||||||
|
login_name: user.id.to_owned(),
|
||||||
|
app_password: generate_app_token(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(ContentType::html())
|
||||||
|
.body(format!(
|
||||||
|
"<!Doctype html><html><body><h1>Hello {}!</h1><p>Login completed, you may close this page.</p></body></html>",
|
||||||
|
user.displayname.unwrap_or(user.id)
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure_nextcloud_login<AP: AuthenticationProvider>(
|
||||||
|
cfg: &mut ServiceConfig,
|
||||||
|
nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||||
|
auth_provider: Arc<AP>,
|
||||||
|
) {
|
||||||
|
cfg.service(
|
||||||
|
web::scope("")
|
||||||
|
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||||
|
.app_data(Data::from(nextcloud_flows_state))
|
||||||
|
.app_data(Data::from(auth_provider.clone()))
|
||||||
|
.service(web::resource("/index.php/login/v2").post(post_nextcloud_login))
|
||||||
|
.service(
|
||||||
|
web::resource("/login/v2/poll/{flow}")
|
||||||
|
.name("nc_login_poll")
|
||||||
|
.post(post_nextcloud_poll::<AP>),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/login/v2/flow/{flow}")
|
||||||
|
.name("nc_login_flow")
|
||||||
|
.get(get_nextcloud_flow),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user