chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
An OAuth login is kept when refreshing it fails
.changeset/oauth-refresh-kept.mdnew+7−0
| @@ -0,0 +1,7 @@ | ||
--- | ||
default: patch | ||
--- | ||
| ||
# An OAuth login is kept when refreshing it fails | ||
| ||
When an OAuth login's refresh failed for any reason, knotless logged in with `KNOTLESS_PASSWORD` instead and saved that over the OAuth login, which can't read private repositories. Now the OAuth login is kept and tried again, and the failure is logged. The password stands in only once the server turns the refresh down for good. | ||
crates/knotless/src/session.rs+12−2
| @@ -150,8 +150,18 @@ pub async fn ensure(ctx: &Ctx, id: &Identity) -> Result<Session> { | ||
if !stored.expiring() { | ||
return Ok(stored); | ||
} | ||
if let Ok(fresh) = refresh(ctx, &stored).await { | ||
return Ok(fresh); | ||
let err = match refresh(ctx, &stored).await { | ||
Ok(fresh) => return Ok(fresh), | ||
Err(err) => format!("{err:#}"), | ||
}; | ||
let did = id.did.as_str(); | ||
tracing::warn!(did, error = err, "login refresh failed"); | ||
// An OAuth login its server has not turned down is kept, to be | ||
// refreshed next time: a password login saved over it would read | ||
// no one's private repository again, and a refresh fails now and | ||
// then as any request does. | ||
if stored.oauth.is_some() && !err.starts_with(oauth::INVALID_GRANT) { | ||
bail!("refreshing the login of {} failed: {err}", id.did); | ||
} | ||
} | ||
// The password is the identifier's: for another account it would log | ||
crates/knotless/tests/library.rs+26−0
| @@ -297,6 +297,32 @@ fn session_refresh_and_fallbacks() { | ||
stale.refresh_jwt = "bad".into(); | ||
std::fs::write(&path, serde_json::to_vec(&stale).unwrap()).unwrap(); | ||
assert!(session::ensure(&ctx, &id).await.is_ok()); | ||
// An OAuth login whose refresh fails, but not as turned down, is | ||
// kept rather than replaced by the password's, which reads no space. | ||
use base64::Engine as _; | ||
let oauth = session::Session { | ||
oauth: Some(knotless::session::OAuth { | ||
key: base64::engine::general_purpose::STANDARD | ||
.encode(knotless::crypto::P256Key::generate().to_bytes()), | ||
issuer: "http://127.0.0.1:1".into(), | ||
token_endpoint: "http://127.0.0.1:1/oauth/token".into(), | ||
revocation_endpoint: None, | ||
client_id: None, | ||
loopback_id: None, | ||
scope: None, | ||
expires_at: 0, | ||
}), | ||
..stale.clone() | ||
}; | ||
std::fs::write(&path, serde_json::to_vec(&oauth).unwrap()).unwrap(); | ||
let err = session::ensure(&ctx, &id).await.unwrap_err(); | ||
assert!( | ||
format!("{err:#}").contains("refreshing the login"), | ||
"{err:#}" | ||
); | ||
let kept: session::Session = | ||
serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap(); | ||
assert_eq!(kept, oauth); | ||
}); | ||
s.set("KNOTLESS_PASSWORD", ""); | ||
s.set( | ||
deploy/runner-host/README.md+6−1
| @@ -132,7 +132,12 @@ which keeps the session and its client key here, never on the main host: | ||
handle>`, signed in to the runner's account at its PDS, and approve. | ||
The session lasts two years if used every three months; `run` takes | ||
it from `/var/lib/knotless-run/bots` from its next start, and the app | ||
password then logs in only if it can no longer be refreshed. | ||
password then logs in only if its server turns the refresh down | ||
(`invalid_grant`). A refresh that fails otherwise is logged (`login | ||
refresh failed`) and tried again, the OAuth session kept: a password | ||
login saved over it would read no private repository again, every one | ||
failing with `Scope does not permit reading`. Should that happen, approve | ||
the login again. | ||
| ||
A private repository made with `knotless repo create --private`, at an appview | ||
that names this runner, makes it a member of the space and invites it; | ||
docs/reference/cli.md+1−1
| @@ -852,7 +852,7 @@ account whose pull asks it, has it read the repositories again after | ||
| Variable | Default | | | ||
| --- | --- | --- | | ||
| `KNOTLESS_IDENTIFIER` | the last login | the account to act as | | ||
| `KNOTLESS_PASSWORD` | | an app password, instead of OAuth: `KNOTLESS_IDENTIFIER`'s, used only to log in as that account, and only when it has no stored session that still refreshes | | ||
| `KNOTLESS_PASSWORD` | | an app password, instead of OAuth: `KNOTLESS_IDENTIFIER`'s, used only to log in as that account, and only when it has no stored session that still refreshes: an OAuth one whose refresh fails other than by its server turning it down (`invalid_grant`) is kept and tried again, the failure logged as `login refresh failed`, rather than replaced by the password's, which reads no private repository | | ||
| `KNOTLESS_BROWSER` | the system's | the command an OAuth login opens its page with | | ||
| `KNOTLESS_SERVICE` | | the PDS every identity is served by, skipping DID resolution | | ||
| `KNOTLESS_PLC_URL` | `https://plc.directory` | the PLC directory | | ||