alpha
Login
or
Join now
microcosm.blue
/
microcosm-rs
Star
0
Fork
3
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
Star
0
Fork
3
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
dev and prod with all the oauth joy
author
phil
date
11 months ago
(Jul 11, 2025, 11:07 AM -0400)
commit
c99e3c33
c99e3c33cb3a0a31745b02db324deb89d1c7b14b
parent
8304c4de
8304c4de85f0a70f38a2fe400ce5e8d29cbfe82d
+254
-24
5 changed files
Expand all
Collapse all
Unified
Split
Cargo.lock
who-am-i
Cargo.toml
src
main.rs
oauth.rs
server.rs
+113
Cargo.lock
Reviewed
···
1162
1162
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
1163
1163
dependencies = [
1164
1164
"const-oid",
1165
1165
+
"pem-rfc7468",
1165
1166
"zeroize",
1166
1167
]
1167
1168
···
1344
1345
"elliptic-curve",
1345
1346
"rfc6979",
1346
1347
"signature",
1348
1348
+
"spki",
1347
1349
]
1348
1350
1349
1351
[[package]]
···
1364
1366
"ff",
1365
1367
"generic-array",
1366
1368
"group",
1369
1369
+
"pem-rfc7468",
1370
1370
+
"pkcs8",
1367
1371
"rand_core 0.6.4",
1368
1372
"sec1",
1369
1373
"subtle",
···
2442
2446
"jose-b64",
2443
2447
"jose-jwa",
2444
2448
"p256",
2449
2449
+
"p384",
2450
2450
+
"rsa",
2445
2451
"serde",
2446
2452
"zeroize",
2447
2453
]
···
2485
2491
version = "1.5.0"
2486
2492
source = "registry+https://github.com/rust-lang/crates.io-index"
2487
2493
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
2494
2494
+
dependencies = [
2495
2495
+
"spin",
2496
2496
+
]
2488
2497
2489
2498
[[package]]
2490
2499
name = "lazycell"
···
2982
2991
]
2983
2992
2984
2993
[[package]]
2994
2994
+
name = "num-bigint-dig"
2995
2995
+
version = "0.8.4"
2996
2996
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2997
2997
+
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
2998
2998
+
dependencies = [
2999
2999
+
"byteorder",
3000
3000
+
"lazy_static",
3001
3001
+
"libm",
3002
3002
+
"num-integer",
3003
3003
+
"num-iter",
3004
3004
+
"num-traits",
3005
3005
+
"rand 0.8.5",
3006
3006
+
"smallvec",
3007
3007
+
"zeroize",
3008
3008
+
]
3009
3009
+
3010
3010
+
[[package]]
2985
3011
name = "num-conv"
2986
3012
version = "0.1.0"
2987
3013
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3007
3033
]
3008
3034
3009
3035
[[package]]
3036
3036
+
name = "num-iter"
3037
3037
+
version = "0.1.45"
3038
3038
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3039
3039
+
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
3040
3040
+
dependencies = [
3041
3041
+
"autocfg",
3042
3042
+
"num-integer",
3043
3043
+
"num-traits",
3044
3044
+
]
3045
3045
+
3046
3046
+
[[package]]
3010
3047
name = "num-modular"
3011
3048
version = "0.6.1"
3012
3049
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3028
3065
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
3029
3066
dependencies = [
3030
3067
"autocfg",
3068
3068
+
"libm",
3031
3069
]
3032
3070
3033
3071
[[package]]
···
3142
3180
]
3143
3181
3144
3182
[[package]]
3183
3183
+
name = "p384"
3184
3184
+
version = "0.13.1"
3185
3185
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3186
3186
+
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
3187
3187
+
dependencies = [
3188
3188
+
"elliptic-curve",
3189
3189
+
"primeorder",
3190
3190
+
]
3191
3191
+
3192
3192
+
[[package]]
3145
3193
name = "parking"
3146
3194
version = "2.2.1"
3147
3195
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3202
3250
dependencies = [
3203
3251
"base64 0.22.1",
3204
3252
"serde",
3253
3253
+
]
3254
3254
+
3255
3255
+
[[package]]
3256
3256
+
name = "pem-rfc7468"
3257
3257
+
version = "0.7.0"
3258
3258
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3259
3259
+
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
3260
3260
+
dependencies = [
3261
3261
+
"base64ct",
3205
3262
]
3206
3263
3207
3264
[[package]]
···
3267
3324
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
3268
3325
3269
3326
[[package]]
3327
3327
+
name = "pkcs1"
3328
3328
+
version = "0.7.5"
3329
3329
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3330
3330
+
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
3331
3331
+
dependencies = [
3332
3332
+
"der",
3333
3333
+
"pkcs8",
3334
3334
+
"spki",
3335
3335
+
]
3336
3336
+
3337
3337
+
[[package]]
3338
3338
+
name = "pkcs8"
3339
3339
+
version = "0.10.2"
3340
3340
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3341
3341
+
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
3342
3342
+
dependencies = [
3343
3343
+
"der",
3344
3344
+
"spki",
3345
3345
+
]
3346
3346
+
3347
3347
+
[[package]]
3270
3348
name = "pkg-config"
3271
3349
version = "0.3.32"
3272
3350
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3666
3744
]
3667
3745
3668
3746
[[package]]
3747
3747
+
name = "rsa"
3748
3748
+
version = "0.9.8"
3749
3749
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3750
3750
+
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
3751
3751
+
dependencies = [
3752
3752
+
"const-oid",
3753
3753
+
"digest",
3754
3754
+
"num-bigint-dig",
3755
3755
+
"num-integer",
3756
3756
+
"num-traits",
3757
3757
+
"pkcs1",
3758
3758
+
"pkcs8",
3759
3759
+
"rand_core 0.6.4",
3760
3760
+
"signature",
3761
3761
+
"spki",
3762
3762
+
"subtle",
3763
3763
+
"zeroize",
3764
3764
+
]
3765
3765
+
3766
3766
+
[[package]]
3669
3767
name = "rustc-demangle"
3670
3768
version = "0.1.24"
3671
3769
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3873
3971
"base16ct",
3874
3972
"der",
3875
3973
"generic-array",
3974
3974
+
"pkcs8",
3876
3975
"subtle",
3877
3976
"zeroize",
3878
3977
]
···
4266
4365
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
4267
4366
dependencies = [
4268
4367
"lock_api",
4368
4368
+
]
4369
4369
+
4370
4370
+
[[package]]
4371
4371
+
name = "spki"
4372
4372
+
version = "0.7.3"
4373
4373
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4374
4374
+
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
4375
4375
+
dependencies = [
4376
4376
+
"base64ct",
4377
4377
+
"der",
4269
4378
]
4270
4379
4271
4380
[[package]]
···
5143
5252
"clap",
5144
5253
"ctrlc",
5145
5254
"dashmap",
5255
5255
+
"elliptic-curve",
5146
5256
"handlebars",
5147
5257
"hickory-resolver",
5258
5258
+
"jose-jwk",
5148
5259
"jsonwebtoken",
5149
5260
"metrics",
5150
5261
"metrics-exporter-prometheus 0.17.2",
5262
5262
+
"p256",
5263
5263
+
"pkcs8",
5151
5264
"rand 0.9.1",
5152
5265
"reqwest",
5153
5266
"serde",
+4
who-am-i/Cargo.toml
Reviewed
···
14
14
clap = { version = "4.5.40", features = ["derive", "env"] }
15
15
ctrlc = "3.4.7"
16
16
dashmap = "6.1.0"
17
17
+
elliptic-curve = "0.13.8"
17
18
handlebars = { version = "6.3.2", features = ["dir_source"] }
18
19
hickory-resolver = "0.25.2"
20
20
+
jose-jwk = "0.1.2"
19
21
jsonwebtoken = "9.3.1"
20
22
metrics = "0.24.2"
23
23
+
p256 = "0.13.2"
24
24
+
pkcs8 = "0.10.2"
21
25
rand = "0.9.1"
22
26
reqwest = { version = "0.12.22", features = ["native-tls-vendored"] }
23
27
serde = { version = "1.0.219", features = ["derive"] }
+38
-1
who-am-i/src/main.rs
Reviewed
···
15
15
/// eg: `cat /dev/urandom | head -c 64 | base64`
16
16
#[arg(long, env)]
17
17
app_secret: String,
18
18
+
/// path to at-oauth private key (PEM pk8 format)
19
19
+
///
20
20
+
/// generate with:
21
21
+
///
22
22
+
/// openssl ecparam -genkey -noout -name prime256v1 \
23
23
+
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem
24
24
+
#[arg(long, env)]
25
25
+
oauth_private_key: Option<PathBuf>,
18
26
/// path to jwt private key (PEM pk8 format)
19
27
///
20
28
/// generate with:
···
34
42
/// wrap the jwk in an array, then in an object under "keys":
35
43
///
36
44
/// { "keys": [<JWK obj>] }
45
45
+
///
46
46
+
/// TODO: remove this, serve automatically
37
47
#[arg(long)]
38
48
jwks: PathBuf,
49
49
+
/// this server's client-reachable base url, for oauth redirect + jwt check
50
50
+
///
51
51
+
/// required unless running in localhost mode with --dev
52
52
+
#[arg(long, env)]
53
53
+
base_url: Option<String>,
54
54
+
/// host:port to bind to on startup
55
55
+
#[arg(long, env, default_value = "127.0.0.1:9997")]
56
56
+
bind: String,
39
57
/// Enable dev mode
40
58
///
41
41
-
/// enables automatic template reloading
59
59
+
/// enables automatic template reloading, uses localhost oauth config, etc
42
60
#[arg(long, action)]
43
61
dev: bool,
44
62
/// Hosts who are allowed to one-click auth
···
57
75
58
76
let args = Args::parse();
59
77
78
78
+
// let bind = args.bind.to_socket_addrs().expect("--bind must be ToSocketAddrs");
79
79
+
80
80
+
let base = args.base_url.unwrap_or_else(|| {
81
81
+
if args.dev {
82
82
+
format!("http://{}", args.bind)
83
83
+
} else {
84
84
+
panic!("not in --dev mode so --base-url is required")
85
85
+
}
86
86
+
});
87
87
+
88
88
+
if !args.dev && args.oauth_private_key.is_none() {
89
89
+
panic!("--at-oauth-key is required except in --dev");
90
90
+
} else if args.dev && args.oauth_private_key.is_some() {
91
91
+
eprintln!("warn: --at-oauth-key is ignored in dev (localhost config)");
92
92
+
}
93
93
+
60
94
if args.allowed_hosts.is_empty() {
61
95
panic!("at least one --allowed-host host must be set");
62
96
}
···
75
109
serve(
76
110
shutdown,
77
111
args.app_secret,
112
112
+
args.oauth_private_key,
78
113
tokens,
114
114
+
base,
115
115
+
args.bind,
79
116
args.allowed_hosts,
80
117
args.dev,
81
118
)
+77
-21
who-am-i/src/oauth.rs
Reviewed
···
1
1
+
use jose_jwk::Class;
2
2
+
use jose_jwk::Jwk;
3
3
+
use jose_jwk::Key;
4
4
+
use jose_jwk::Parameters;
5
5
+
use std::fs;
6
6
+
use std::path::PathBuf;
7
7
+
// use p256::SecretKey;
1
8
use atrium_api::{agent::SessionManager, types::string::Did};
2
9
use atrium_common::resolver::Resolver;
3
10
use atrium_identity::{
···
5
12
handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig, DnsTxtResolver},
6
13
};
7
14
use atrium_oauth::{
8
8
-
AtprotoLocalhostClientMetadata, AuthorizeOptions, CallbackParams, DefaultHttpClient,
9
9
-
KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope,
15
15
+
AtprotoClientMetadata, AtprotoLocalhostClientMetadata, AuthMethod, AuthorizeOptions,
16
16
+
CallbackParams, DefaultHttpClient, GrantType, KnownScope, OAuthClient, OAuthClientConfig,
17
17
+
OAuthClientMetadata, OAuthResolverConfig, Scope,
10
18
store::{session::MemorySessionStore, state::MemoryStateStore},
11
19
};
20
20
+
use elliptic_curve::SecretKey;
12
21
use hickory_resolver::{ResolveError, TokioResolver};
22
22
+
use jose_jwk::JwkSet;
23
23
+
use pkcs8::DecodePrivateKey;
13
24
use serde::Deserialize;
14
25
use std::sync::Arc;
15
26
use thiserror::Error;
···
83
94
}
84
95
85
96
impl OAuth {
86
86
-
pub fn new() -> Result<Self, AuthSetupError> {
97
97
+
pub fn new(oauth_private_key: Option<PathBuf>, base: String) -> Result<Self, AuthSetupError> {
87
98
let http_client = Arc::new(DefaultHttpClient::default());
88
99
let did_resolver = || {
89
100
CommonDidResolver::new(CommonDidResolverConfig {
···
93
104
};
94
105
let dns_txt_resolver =
95
106
HickoryDnsTxtResolver::new().map_err(AuthSetupError::HickoryResolverError)?;
96
96
-
let client_config = OAuthClientConfig {
97
97
-
client_metadata: AtprotoLocalhostClientMetadata {
98
98
-
redirect_uris: Some(vec![String::from("http://127.0.0.1:9997/authorized")]),
99
99
-
scopes: Some(READONLY_SCOPE.to_vec()),
100
100
-
},
101
101
-
keys: None,
102
102
-
resolver: OAuthResolverConfig {
103
103
-
did_resolver: did_resolver(),
104
104
-
handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig {
105
105
-
dns_txt_resolver,
106
106
-
http_client: Arc::clone(&http_client),
107
107
-
}),
108
108
-
authorization_server_metadata: Default::default(),
109
109
-
protected_resource_metadata: Default::default(),
110
110
-
},
111
111
-
state_store: MemoryStateStore::default(),
112
112
-
session_store: MemorySessionStore::default(),
107
107
+
108
108
+
let resolver = OAuthResolverConfig {
109
109
+
did_resolver: did_resolver(),
110
110
+
handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig {
111
111
+
dns_txt_resolver,
112
112
+
http_client: Arc::clone(&http_client),
113
113
+
}),
114
114
+
authorization_server_metadata: Default::default(),
115
115
+
protected_resource_metadata: Default::default(),
113
116
};
114
117
115
115
-
let client = OAuthClient::new(client_config).map_err(AuthSetupError::AtriumClientError)?;
118
118
+
let state_store = MemoryStateStore::default();
119
119
+
let session_store = MemorySessionStore::default();
120
120
+
121
121
+
let client = if let Some(path) = oauth_private_key {
122
122
+
let key_contents: Vec<u8> = fs::read(path).unwrap();
123
123
+
let key_string = String::from_utf8(key_contents).unwrap();
124
124
+
let key = SecretKey::<p256::NistP256>::from_pkcs8_pem(&key_string)
125
125
+
.map(|secret_key| Jwk {
126
126
+
key: Key::from(&secret_key.into()),
127
127
+
prm: Parameters {
128
128
+
kid: Some("at-oauth-00".to_string()),
129
129
+
cls: Some(Class::Signing),
130
130
+
..Default::default()
131
131
+
},
132
132
+
})
133
133
+
.expect("to get private key");
134
134
+
OAuthClient::new(OAuthClientConfig {
135
135
+
client_metadata: AtprotoClientMetadata {
136
136
+
client_id: format!("{base}/client-metadata.json"),
137
137
+
client_uri: Some(base.clone()),
138
138
+
redirect_uris: vec![format!("{base}/authorized")],
139
139
+
token_endpoint_auth_method: AuthMethod::PrivateKeyJwt,
140
140
+
grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
141
141
+
scopes: READONLY_SCOPE.to_vec(),
142
142
+
jwks_uri: Some(format!("{base}/.well-known/at-jwks.json")),
143
143
+
token_endpoint_auth_signing_alg: Some(String::from("ES256")),
144
144
+
},
145
145
+
keys: Some(vec![key]),
146
146
+
resolver,
147
147
+
state_store,
148
148
+
session_store,
149
149
+
})
150
150
+
.map_err(AuthSetupError::AtriumClientError)?
151
151
+
} else {
152
152
+
OAuthClient::new(OAuthClientConfig {
153
153
+
client_metadata: AtprotoLocalhostClientMetadata {
154
154
+
redirect_uris: Some(vec![String::from("http://127.0.0.1:9997/authorized")]),
155
155
+
scopes: Some(READONLY_SCOPE.to_vec()),
156
156
+
},
157
157
+
keys: None,
158
158
+
resolver,
159
159
+
state_store,
160
160
+
session_store,
161
161
+
})
162
162
+
.map_err(AuthSetupError::AtriumClientError)?
163
163
+
};
116
164
117
165
Ok(Self {
118
166
client: Arc::new(client),
119
167
did_resolver: Arc::new(did_resolver()),
120
168
})
169
169
+
}
170
170
+
171
171
+
pub fn client_metadata(&self) -> OAuthClientMetadata {
172
172
+
self.client.client_metadata.clone()
173
173
+
}
174
174
+
175
175
+
pub fn jwks(&self) -> JwkSet {
176
176
+
self.client.jwks()
121
177
}
122
178
123
179
pub async fn begin(&self, handle: &str) -> Result<String, atrium_oauth::Error> {
+22
-2
who-am-i/src/server.rs
Reviewed
···
1
1
use atrium_api::types::string::Did;
2
2
+
use atrium_oauth::OAuthClientMetadata;
2
3
use axum::{
3
4
Router,
4
5
extract::{FromRef, Json as ExtractJson, Query, State},
···
12
13
use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar};
13
14
use axum_template::{RenderHtml, engine::Engine};
14
15
use handlebars::{Handlebars, handlebars_helper};
16
16
+
use jose_jwk::JwkSet;
17
17
+
use std::path::PathBuf;
15
18
16
19
use serde::Deserialize;
17
20
use serde_json::{Value, json};
···
52
55
}
53
56
}
54
57
58
58
+
#[allow(clippy::too_many_arguments)]
55
59
pub async fn serve(
56
60
shutdown: CancellationToken,
57
61
app_secret: String,
62
62
+
oauth_private_key: Option<PathBuf>,
58
63
tokens: Tokens,
64
64
+
base: String,
65
65
+
bind: String,
59
66
allowed_hosts: Vec<String>,
60
67
dev: bool,
61
68
) {
···
70
77
// clients have to pick up their identity-resolving tasks within this period
71
78
let task_pickup_expiration = Duration::from_secs(15);
72
79
73
73
-
let oauth = OAuth::new().unwrap();
80
80
+
let oauth = OAuth::new(oauth_private_key, base).unwrap();
74
81
75
82
let state = AppState {
76
83
engine: Engine::new(hbs),
···
88
95
.route("/style.css", get(css))
89
96
.route("/prompt", get(prompt))
90
97
.route("/user-info", post(user_info))
98
98
+
.route("/client-metadata.json", get(client_metadata))
91
99
.route("/auth", get(start_oauth))
92
100
.route("/authorized", get(complete_oauth))
93
101
.route("/disconnect", post(disconnect))
102
102
+
.route("/.well-known/at-jwks.json", get(at_jwks)) // todo combine jwks eps (key id is enough?)
94
103
.route("/.well-known/jwks.json", get(jwks))
95
104
.with_state(state);
96
105
97
97
-
let listener = TcpListener::bind("0.0.0.0:9997")
106
106
+
eprintln!("starting server at http://{bind}");
107
107
+
let listener = TcpListener::bind(bind)
98
108
.await
99
109
.expect("listener binding to work");
100
110
···
297
307
Json(json!({ "handle": handle })).into_response()
298
308
}
299
309
}
310
310
+
}
311
311
+
312
312
+
async fn client_metadata(
313
313
+
State(AppState { oauth, .. }): State<AppState>,
314
314
+
) -> Json<OAuthClientMetadata> {
315
315
+
Json(oauth.client_metadata())
316
316
+
}
317
317
+
318
318
+
async fn at_jwks(State(AppState { oauth, .. }): State<AppState>) -> Json<JwkSet> {
319
319
+
Json(oauth.jwks())
300
320
}
301
321
302
322
#[derive(Debug, Deserialize)]