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

show current session at hello and allow revoking

do we need csrf here or...

Changed files
+107 -11
who-am-i
src
static
templates
+34 -7
who-am-i/src/server.rs
···
header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER, X_FRAME_OPTIONS},
},
response::{IntoResponse, Json, Redirect, Response},
-
routing::get,
+
routing::{get, post},
};
use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar};
use axum_template::{RenderHtml, engine::Engine};
···
.route("/user-info", get(user_info))
.route("/auth", get(start_oauth))
.route("/authorized", get(complete_oauth))
+
.route("/disconnect", post(disconnect))
.with_state(state);
let listener = TcpListener::bind("0.0.0.0:9997")
···
}
async fn hello(
-
State(AppState { engine, .. }): State<AppState>,
+
State(AppState {
+
engine,
+
resolve_handles,
+
shutdown,
+
oauth,
+
..
+
}): State<AppState>,
mut jar: SignedCookieJar,
) -> Response {
-
// push expiry (or clean up) the current cookie
-
if let Some(did) = jar.get(DID_COOKIE_KEY) {
+
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
jar = jar.add(cookie(&did));
+
let fetch_key = resolve_handles.dispatch(
+
{
+
let oauth = oauth.clone();
+
let did = did.clone();
+
async move { oauth.resolve_handle(did.clone()).await }
+
},
+
shutdown.child_token(),
+
);
+
json!({
+
"did": did,
+
"fetch_key": fetch_key,
+
})
} else {
jar = jar.remove(DID_COOKIE_KEY);
+
json!({})
}
-
}
+
} else {
+
json!({})
+
};
let frame_headers = [
(X_FRAME_OPTIONS, "deny"),
(CONTENT_SECURITY_POLICY, "frame-ancestors 'none'"),
];
-
(frame_headers, jar, RenderHtml("hello", engine, json!({}))).into_response()
+
(frame_headers, jar, RenderHtml("hello", engine, info)).into_response()
}
async fn css() -> impl IntoResponse {
···
(X_FRAME_OPTIONS, format!("allow-from {parent_origin}")),
(
CONTENT_SECURITY_POLICY,
-
format!("frame-ancestors {parent_host}"),
+
format!("frame-ancestors {parent_origin}"),
),
];
···
});
(jar, RenderHtml("authorized", engine, info)).into_response()
}
+
+
async fn disconnect(jar: SignedCookieJar) -> impl IntoResponse {
+
let jar = jar.remove(DID_COOKIE_KEY);
+
(jar, Json(json!({ "ok": true })))
+
}
+5 -1
who-am-i/static/style.css
···
}
#connect,
-
#allow {
+
#allow,
+
#revoke {
background: transparent;
border: none;
border-left: 1px solid #bbb;
···
color: #375;
font: inherit;
cursor: pointer;
+
}
+
#revoke {
+
color: #a31;
}
#action:hover #allow {
color: #285;
+68 -3
who-am-i/templates/hello.hbs
···
{{#*inline "description"}}A little identity-verifying auth service for microcosm demos{{/inline}}
{{#*inline "main"}}
-
<div class="mini-content">
-
This is a little identity-verifying service for microcosm demos.
-
</div>
+
<div class="mini-content">
+
This is a little identity-verifying service for microcosm demos.
+
+
{{#if did}}
+
<p id="error-message" class="hidden"></p>
+
+
<p id="prompt" class="detail">
+
Connected identity:
+
</p>
+
+
<div id="loader">
+
<span class="spinner"></span>
+
</div>
+
+
<div id="user-info">
+
<div id="handle-action" class="action">
+
<span id="handle-view" class="handle"></span>
+
<button id="revoke">disconnect</button>
+
</div>
+
</div>
+
<script>
+
const errorEl = document.getElementById('error-message');
+
const loaderEl = document.getElementById('loader');
+
const handleViewEl = document.getElementById('handle-view');
+
const revokeEl = document.getElementById('revoke'); // for known-did
+
+
function err(e, msg) {
+
loaderEl.classList.add('hidden');
+
errorEl.classList.remove('hidden');
+
errorEl.textContent = msg || e;
+
throw new Error(e);
+
}
+
+
// already-known user
+
({{{json did}}}) && (async () => {
+
+
const handle = await lookUp({{{json fetch_key}}});
+
console.log('got handle', handle);
+
+
loaderEl.classList.add('hidden');
+
handleViewEl.textContent = `@${handle}`;
+
revokeEl.addEventListener('click', async () => {
+
try {
+
let res = await fetch('/disconnect', { method: 'POST', credentials: 'include' });
+
if (!res.ok) throw res;
+
} catch (e) {
+
err(e, 'failed to clear session, sorry');
+
}
+
window.location.reload();
+
});
+
})();
+
+
async function lookUp(fetch_key) {
+
const user_info = new URL('/user-info', window.location);
+
user_info.searchParams.set('fetch-key', fetch_key);
+
let info;
+
try {
+
const resp = await fetch(user_info);
+
if (!resp.ok) throw resp;
+
info = await resp.json();
+
} catch (e) {
+
err(e, 'failed to resolve handle from DID')
+
}
+
return info.handle;
+
}
+
</script>
+
{{/if}}
+
</div>
{{/inline}}
{{#> base-full}}{{/base-full}}