Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

desparate attempts for cross-browser compat

safari might be a lost cause

who-am-i may have been a mistake

Changed files
+123 -10
who-am-i
+44 -4
who-am-i/src/server.rs
···
response::{IntoResponse, Json, Redirect, Response},
routing::{get, post},
};
-
use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar};
+
use axum_extra::extract::cookie::{Cookie, Expiration, Key, SameSite, SignedCookieJar};
use axum_template::{RenderHtml, engine::Engine};
use handlebars::{Handlebars, handlebars_helper};
use jose_jwk::JwkSet;
···
use serde_json::{Value, json};
use std::collections::HashSet;
use std::sync::Arc;
-
use std::time::Duration;
+
use std::time::{Duration, SystemTime};
use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken;
use url::Url;
···
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
const STYLE_CSS: &str = include_str!("../static/style.css");
+
const HELLO_COOKIE_KEY: &str = "hello-who-am-i";
const DID_COOKIE_KEY: &str = "did";
const COOKIE_EXPIRATION: Duration = Duration::from_secs(30 * 86_400);
···
.unwrap();
}
+
#[derive(Debug, Deserialize)]
+
struct HelloQuery {
+
auth_reload: Option<String>,
+
auth_failed: Option<String>,
+
}
async fn hello(
State(AppState {
engine,
···
oauth,
..
}): State<AppState>,
+
Query(params): Query<HelloQuery>,
mut jar: SignedCookieJar,
) -> Response {
+
let is_auth_reload = params.auth_reload.is_some();
+
let auth_failed = params.auth_failed.is_some();
+
let no_cookie = jar.get(HELLO_COOKIE_KEY).is_none();
+
jar = jar.add(hello_cookie());
+
let info = if let Some(did) = jar.get(DID_COOKIE_KEY) {
if let Ok(did) = Did::new(did.value_trimmed().to_string()) {
// push cookie expiry
···
json!({
"did": did,
"fetch_key": fetch_key,
+
"is_auth_reload": is_auth_reload,
+
"auth_failed": auth_failed,
+
"no_cookie": no_cookie,
})
} else {
jar = jar.remove(DID_COOKIE_KEY);
-
json!({})
+
json!({
+
"is_auth_reload": is_auth_reload,
+
"auth_failed": auth_failed,
+
"no_cookie": no_cookie,
+
})
}
} else {
-
json!({})
+
json!({
+
"is_auth_reload": is_auth_reload,
+
"auth_failed": auth_failed,
+
"no_cookie": no_cookie,
+
})
};
let frame_headers = [(CONTENT_SECURITY_POLICY, "frame-ancestors 'none'")];
(frame_headers, jar, RenderHtml("hello", engine, info)).into_response()
···
([(CONTENT_TYPE, "image/x-icon")], FAVICON)
}
+
fn hello_cookie() -> Cookie<'static> {
+
Cookie::build((HELLO_COOKIE_KEY, "hiiii"))
+
.http_only(true)
+
.secure(true)
+
.same_site(SameSite::None)
+
.expires(Expiration::DateTime(
+
(SystemTime::now() + COOKIE_EXPIRATION).into(),
+
)) // wtf safari needs this to not be a session cookie??
+
.max_age(COOKIE_EXPIRATION.try_into().unwrap())
+
.path("/")
+
.into()
+
}
+
fn cookie(did: &Did) -> Cookie<'static> {
Cookie::build((DID_COOKIE_KEY, did.to_string()))
.http_only(true)
.secure(true)
.same_site(SameSite::None)
+
.expires(Expiration::DateTime(
+
(SystemTime::now() + COOKIE_EXPIRATION).into(),
+
)) // wtf safari needs this to not be a session cookie??
.max_age(COOKIE_EXPIRATION.try_into().unwrap())
+
.path("/")
.into()
}
+11
who-am-i/static/style.css
···
color: #285;
}
+
#need-storage {
+
font-size: 0.8rem;
+
}
+
.problem {
+
color: #a31;
+
}
+
#or {
font-size: 0.8rem;
text-align: center;
···
.hidden {
display: none !important;
}
+
+
.hello-connect-plz {
+
margin: 1.667rem 0 0.667rem;
+
}
+5 -1
who-am-i/templates/authorized.hbs
···
<!doctype html>
+
<meta charset="utf-8" />
+
<title>great job!</title>
-
<p>oh sick. hey {{ did }}. you can close this window now.</p>
+
<h1>oauth success!</h1>
+
<p>this window should automatically close itself (probably a bug if it hasn't)</p>
<script>
// TODO: tie this back to its source...........
···
token: {{{json token}}},
fetch_key: {{{json fetch_key}}},
}));
+
// TODO: probably also wait for a reply from the frame and show an error if not
window.close();
</script>
+45 -4
who-am-i/templates/hello.hbs
···
<div class="mini-content">
<div class="explain">
<p>This is a little identity-verifying service for microcosm demos.</p>
+
<p>Only <strong>read access to your public data</strong> is required to connect: connecting does not grant any ability to modify your account or data.</p>
</div>
{{#if did}}
···
} catch (e) {
err(e, 'failed to clear session, sorry');
}
-
window.location.reload();
+
window.location.replace(location.pathname);
+
window.location.reload(); // backup, in case there is no query?
});
})();
···
}
</script>
{{else}}
-
<p id="prompt" class="detail no">
-
No identity connected.
-
</p>
+
+
<p class="hello-connect-plz">Connect your handle</p>
+
+
{{#if is_auth_reload}}
+
{{#if no_cookie}}
+
<p id="prompt" class="detail no">
+
No identity connected. Your browser may be blocking access for connecting.
+
</p>
+
{{else}}
+
{{#if auth_failed}}
+
<p id="prompt" class="detail no">
+
No identity connected. Connecting failed or was denied.
+
</p>
+
{{else}}
+
<p id="prompt" class="detail no">
+
No identity connected.
+
</p>
+
{{/if}}
+
{{/if}}
+
{{/if}}
+
+
<div id="user-info">
+
<form id="form-action" action="/auth" target="_blank" method="GET" class="action {{#if did}}hidden{{/if}}">
+
<label>
+
@<input id="handle-input" class="handle" name="handle" placeholder="example.bsky.social" />
+
</label>
+
<button id="connect" type="submit">connect</button>
+
</form>
+
</div>
{{/if}}
+
</div>
+
<script>
+
window.addEventListener('storage', e => {
+
console.log('eyyy got storage', e);
+
if (e.key !== 'who-am-i') return;
+
if (!e.newValue) return;
+
if (e.newValue.result === 'success') {
+
window.location = '/?auth_reload=1';
+
} else {
+
window.location = '/?auth_reload=1&auth_failed=1';
+
}
+
});
+
</script>
{{/inline}}
{{#> base-full}}{{/base-full}}
+18 -1
who-am-i/templates/prompt.hbs
···
</div>
</div>
+
<div id="need-storage" class="hidden">
+
<p class="problem">Sorry, your browser is blocking access.</p>
+
<p>Try <a href="/" target="_blank">connecting directly</a> first (but no promises).</p>
+
</div>
+
<script>
···
const formEl = document.getElementById('form-action'); // for anon
const allowEl = document.getElementById('handle-action'); // for known-did
const connectEl = document.getElementById('connect'); // for anon
+
const needStorageEl = document.getElementById('need-storage'); // for safari/frame isolation
function err(e, msg) {
loaderEl.classList.add('hidden');
···
window.open(url, '_blank');
};
+
// check if we may be partitioned, preventing access after auth completion
+
// this should only happen if on a browser that implements storage access api
+
if ('hasStorageAccess' in document) {
+
document.hasStorageAccess().then((hasAccess) => {
+
if (!hasAccess) {
+
promptEl.classList.add('hidden');
+
infoEl.classList.add('hidden');
+
needStorageEl.classList.remove('hidden');
+
}
+
});
+
}
+
window.addEventListener('storage', async e => {
// here's a fun minor vuln: we can't tell which flow triggers the storage event.
// so if you have two flows going, it grants for both (or the first responder?) if you grant for either.
···
console.error("hmm, heard from localstorage but did not get DID", details, e);
err('sorry, something went wrong getting your details');
}
-
localStorage.removeItem(e.key);
let parsed;
try {