diff --git a/Cargo.lock b/Cargo.lock
index 28705c8..fac9a31 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1964,6 +1964,16 @@ dependencies = [
"unicode-segmentation",
]
+[[package]]
+name = "keyring"
+version = "3.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
+dependencies = [
+ "log",
+ "zeroize",
+]
+
[[package]]
name = "libappindicator"
version = "0.9.0"
@@ -2175,7 +2185,10 @@ name = "nextcloud_client"
version = "0.1.0"
dependencies = [
"bytes",
+ "dirs",
"futures-util",
+ "keyring",
+ "open",
"pin-project-lite",
"reqwest 0.12.28",
"serde",
diff --git a/Tasks.md b/Tasks.md
index b088916..8ab556e 100644
--- a/Tasks.md
+++ b/Tasks.md
@@ -74,7 +74,7 @@ This document defines the complete project roadmap and task tracking system for
| [NUT-003](#nut-003) | Implement WebDAV Upload Logic | Fixed | Feature |
| [NUT-004](#nut-004) | Implement OCS Share Link Generation | Fixed | Feature |
| [NUT-005](#nut-005) | Implement Direct Download URL Builder | Triage | Feature |
-| [NUT-006](#nut-006) | Implement Credential Storage System | Triage | Feature |
+| [NUT-006](#nut-006) | Implement Credential Storage System | Fixed | Feature |
| [NUT-007](#nut-007) | Implement Multi-Account Support (Backend) | Triage | Feature |
| [NUT-008](#nut-008) | Implement CLI Frontend | Triage | Feature |
| [NUT-009](#nut-009) | Implement CLI Output Formatting Options | Triage | Feature |
@@ -192,22 +192,22 @@ Generate direct-download URLs from share tokens.
- NUT-004
-
+
### Implement Credential Storage System
**ID:** NUT-006
-**Status:** Triage
+**Status:** Fixed
**Type:** Feature
**Description:**
Implement secure credential storage using OS keychain when available, falling back to encrypted config files. Support Nextcloud Login Flow v2 (`/index.php/login/v2`) for browser-based interactive authentication (supporting 2FA/SSO) alongside manual app password entry.
**Requirements:**
-- [ ] Implement Nextcloud Login Flow v2 client (initiate + browser open + polling)
-- [ ] macOS Keychain support
-- [ ] Windows Credential Manager support
-- [ ] Linux Secret Service support
-- [ ] Encrypted fallback file
-- [ ] Store server URL, username, app password
+- [x] Implement Nextcloud Login Flow v2 client (initiate + browser open + polling)
+- [x] macOS Keychain support
+- [x] Windows Credential Manager support
+- [x] Linux Secret Service support
+- [x] Encrypted fallback file
+- [x] Store server URL, username, app password
**Dependencies:**
- NUT-002
diff --git a/nextcloud_client/Cargo.toml b/nextcloud_client/Cargo.toml
index 212952a..e27bcfc 100644
--- a/nextcloud_client/Cargo.toml
+++ b/nextcloud_client/Cargo.toml
@@ -14,3 +14,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"
url = { version = "2.5", features = ["serde"] }
+keyring = "3"
+dirs = "6.0"
+open = "5.3"
diff --git a/nextcloud_client/src/auth.rs b/nextcloud_client/src/auth.rs
new file mode 100644
index 0000000..642d132
--- /dev/null
+++ b/nextcloud_client/src/auth.rs
@@ -0,0 +1,161 @@
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::time::Duration;
+use tokio::time::sleep;
+use url::Url;
+
+use crate::error::{NextcloudError, Result};
+
+/// Response received when initiating Nextcloud Login Flow v2.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct LoginFlowInitResponse {
+ /// Information needed to poll for the completed authorization.
+ pub poll: LoginFlowPollInfo,
+
+ /// Web browser URL that the user visits to grant access.
+ pub login: String,
+}
+
+/// Polling endpoint details for Login Flow v2.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct LoginFlowPollInfo {
+ /// Polling token associated with the session.
+ pub token: String,
+
+ /// Polling endpoint URL (e.g. `https://cloud.example.com/index.php/login/v2/poll`).
+ pub endpoint: String,
+}
+
+/// Successful credential payload returned once the user clicks 'Grant access' in their browser.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LoginFlowPollSuccess {
+ /// The canonical server URL reported by Nextcloud.
+ pub server: String,
+
+ /// The username of the authenticated user.
+ pub login_name: String,
+
+ /// The generated unique App Password token.
+ pub app_password: String,
+}
+
+/// Initiates Nextcloud Login Flow v2 against a given server URL.
+///
+/// Sends `POST /index.php/login/v2` and returns the browser login URL and polling token.
+pub async fn initiate_login_flow(
+ http: &reqwest::Client,
+ server_url: &Url,
+) -> Result {
+ let init_url = server_url.join("index.php/login/v2")?;
+
+ let response = http.post(init_url).send().await?.error_for_status()?;
+ let payload: LoginFlowInitResponse = response.json().await?;
+ Ok(payload)
+}
+
+/// Poll the Nextcloud Login Flow v2 endpoint once.
+///
+/// Returns:
+/// - `Ok(Some(creds))` when user has completed login in browser.
+/// - `Ok(None)` while user has not yet authorized (HTTP 404 response).
+/// - `Err(NextcloudError)` on network or server errors.
+pub async fn poll_login_flow(
+ http: &reqwest::Client,
+ endpoint: &str,
+ token: &str,
+) -> Result