+113
Cargo.lock
+113
Cargo.lock
···
1162
1162
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
1163
1163
dependencies = [
1164
1164
"const-oid",
1165
+
"pem-rfc7468",
1165
1166
"zeroize",
1166
1167
]
1167
1168
···
1344
1345
"elliptic-curve",
1345
1346
"rfc6979",
1346
1347
"signature",
1348
+
"spki",
1347
1349
]
1348
1350
1349
1351
[[package]]
···
1364
1366
"ff",
1365
1367
"generic-array",
1366
1368
"group",
1369
+
"pem-rfc7468",
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
+
"p384",
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
+
dependencies = [
2495
+
"spin",
2496
+
]
2488
2497
2489
2498
[[package]]
2490
2499
name = "lazycell"
···
2982
2991
]
2983
2992
2984
2993
[[package]]
2994
+
name = "num-bigint-dig"
2995
+
version = "0.8.4"
2996
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2997
+
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
2998
+
dependencies = [
2999
+
"byteorder",
3000
+
"lazy_static",
3001
+
"libm",
3002
+
"num-integer",
3003
+
"num-iter",
3004
+
"num-traits",
3005
+
"rand 0.8.5",
3006
+
"smallvec",
3007
+
"zeroize",
3008
+
]
3009
+
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
+
name = "num-iter"
3037
+
version = "0.1.45"
3038
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3039
+
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
3040
+
dependencies = [
3041
+
"autocfg",
3042
+
"num-integer",
3043
+
"num-traits",
3044
+
]
3045
+
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
+
"libm",
3031
3069
]
3032
3070
3033
3071
[[package]]
···
3142
3180
]
3143
3181
3144
3182
[[package]]
3183
+
name = "p384"
3184
+
version = "0.13.1"
3185
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3186
+
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
3187
+
dependencies = [
3188
+
"elliptic-curve",
3189
+
"primeorder",
3190
+
]
3191
+
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
+
]
3254
+
3255
+
[[package]]
3256
+
name = "pem-rfc7468"
3257
+
version = "0.7.0"
3258
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3259
+
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
3260
+
dependencies = [
3261
+
"base64ct",
3205
3262
]
3206
3263
3207
3264
[[package]]
···
3267
3324
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
3268
3325
3269
3326
[[package]]
3327
+
name = "pkcs1"
3328
+
version = "0.7.5"
3329
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3330
+
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
3331
+
dependencies = [
3332
+
"der",
3333
+
"pkcs8",
3334
+
"spki",
3335
+
]
3336
+
3337
+
[[package]]
3338
+
name = "pkcs8"
3339
+
version = "0.10.2"
3340
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3341
+
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
3342
+
dependencies = [
3343
+
"der",
3344
+
"spki",
3345
+
]
3346
+
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
+
name = "rsa"
3748
+
version = "0.9.8"
3749
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3750
+
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
3751
+
dependencies = [
3752
+
"const-oid",
3753
+
"digest",
3754
+
"num-bigint-dig",
3755
+
"num-integer",
3756
+
"num-traits",
3757
+
"pkcs1",
3758
+
"pkcs8",
3759
+
"rand_core 0.6.4",
3760
+
"signature",
3761
+
"spki",
3762
+
"subtle",
3763
+
"zeroize",
3764
+
]
3765
+
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
+
"pkcs8",
3876
3975
"subtle",
3877
3976
"zeroize",
3878
3977
]
···
4266
4365
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
4267
4366
dependencies = [
4268
4367
"lock_api",
4368
+
]
4369
+
4370
+
[[package]]
4371
+
name = "spki"
4372
+
version = "0.7.3"
4373
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4374
+
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
4375
+
dependencies = [
4376
+
"base64ct",
4377
+
"der",
4269
4378
]
4270
4379
4271
4380
[[package]]
···
5143
5252
"clap",
5144
5253
"ctrlc",
5145
5254
"dashmap",
5255
+
"elliptic-curve",
5146
5256
"handlebars",
5147
5257
"hickory-resolver",
5258
+
"jose-jwk",
5148
5259
"jsonwebtoken",
5149
5260
"metrics",
5150
5261
"metrics-exporter-prometheus 0.17.2",
5262
+
"p256",
5263
+
"pkcs8",
5151
5264
"rand 0.9.1",
5152
5265
"reqwest",
5153
5266
"serde",
+4
who-am-i/Cargo.toml
+4
who-am-i/Cargo.toml
···
14
14
clap = { version = "4.5.40", features = ["derive", "env"] }
15
15
ctrlc = "3.4.7"
16
16
dashmap = "6.1.0"
17
+
elliptic-curve = "0.13.8"
17
18
handlebars = { version = "6.3.2", features = ["dir_source"] }
18
19
hickory-resolver = "0.25.2"
20
+
jose-jwk = "0.1.2"
19
21
jsonwebtoken = "9.3.1"
20
22
metrics = "0.24.2"
23
+
p256 = "0.13.2"
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
+38
-1
who-am-i/src/main.rs
···
15
15
/// eg: `cat /dev/urandom | head -c 64 | base64`
16
16
#[arg(long, env)]
17
17
app_secret: String,
18
+
/// path to at-oauth private key (PEM pk8 format)
19
+
///
20
+
/// generate with:
21
+
///
22
+
/// openssl ecparam -genkey -noout -name prime256v1 \
23
+
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem
24
+
#[arg(long, env)]
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
+
///
46
+
/// TODO: remove this, serve automatically
37
47
#[arg(long)]
38
48
jwks: PathBuf,
49
+
/// this server's client-reachable base url, for oauth redirect + jwt check
50
+
///
51
+
/// required unless running in localhost mode with --dev
52
+
#[arg(long, env)]
53
+
base_url: Option<String>,
54
+
/// host:port to bind to on startup
55
+
#[arg(long, env, default_value = "127.0.0.1:9997")]
56
+
bind: String,
39
57
/// Enable dev mode
40
58
///
41
-
/// enables automatic template reloading
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
+
// let bind = args.bind.to_socket_addrs().expect("--bind must be ToSocketAddrs");
79
+
80
+
let base = args.base_url.unwrap_or_else(|| {
81
+
if args.dev {
82
+
format!("http://{}", args.bind)
83
+
} else {
84
+
panic!("not in --dev mode so --base-url is required")
85
+
}
86
+
});
87
+
88
+
if !args.dev && args.oauth_private_key.is_none() {
89
+
panic!("--at-oauth-key is required except in --dev");
90
+
} else if args.dev && args.oauth_private_key.is_some() {
91
+
eprintln!("warn: --at-oauth-key is ignored in dev (localhost config)");
92
+
}
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
+
args.oauth_private_key,
78
113
tokens,
114
+
base,
115
+
args.bind,
79
116
args.allowed_hosts,
80
117
args.dev,
81
118
)
+77
-21
who-am-i/src/oauth.rs
+77
-21
who-am-i/src/oauth.rs
···
1
+
use jose_jwk::Class;
2
+
use jose_jwk::Jwk;
3
+
use jose_jwk::Key;
4
+
use jose_jwk::Parameters;
5
+
use std::fs;
6
+
use std::path::PathBuf;
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
-
AtprotoLocalhostClientMetadata, AuthorizeOptions, CallbackParams, DefaultHttpClient,
9
-
KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope,
15
+
AtprotoClientMetadata, AtprotoLocalhostClientMetadata, AuthMethod, AuthorizeOptions,
16
+
CallbackParams, DefaultHttpClient, GrantType, KnownScope, OAuthClient, OAuthClientConfig,
17
+
OAuthClientMetadata, OAuthResolverConfig, Scope,
10
18
store::{session::MemorySessionStore, state::MemoryStateStore},
11
19
};
20
+
use elliptic_curve::SecretKey;
12
21
use hickory_resolver::{ResolveError, TokioResolver};
22
+
use jose_jwk::JwkSet;
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
-
pub fn new() -> Result<Self, AuthSetupError> {
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
-
let client_config = OAuthClientConfig {
97
-
client_metadata: AtprotoLocalhostClientMetadata {
98
-
redirect_uris: Some(vec![String::from("http://127.0.0.1:9997/authorized")]),
99
-
scopes: Some(READONLY_SCOPE.to_vec()),
100
-
},
101
-
keys: None,
102
-
resolver: OAuthResolverConfig {
103
-
did_resolver: did_resolver(),
104
-
handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig {
105
-
dns_txt_resolver,
106
-
http_client: Arc::clone(&http_client),
107
-
}),
108
-
authorization_server_metadata: Default::default(),
109
-
protected_resource_metadata: Default::default(),
110
-
},
111
-
state_store: MemoryStateStore::default(),
112
-
session_store: MemorySessionStore::default(),
107
+
108
+
let resolver = OAuthResolverConfig {
109
+
did_resolver: did_resolver(),
110
+
handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig {
111
+
dns_txt_resolver,
112
+
http_client: Arc::clone(&http_client),
113
+
}),
114
+
authorization_server_metadata: Default::default(),
115
+
protected_resource_metadata: Default::default(),
113
116
};
114
117
115
-
let client = OAuthClient::new(client_config).map_err(AuthSetupError::AtriumClientError)?;
118
+
let state_store = MemoryStateStore::default();
119
+
let session_store = MemorySessionStore::default();
120
+
121
+
let client = if let Some(path) = oauth_private_key {
122
+
let key_contents: Vec<u8> = fs::read(path).unwrap();
123
+
let key_string = String::from_utf8(key_contents).unwrap();
124
+
let key = SecretKey::<p256::NistP256>::from_pkcs8_pem(&key_string)
125
+
.map(|secret_key| Jwk {
126
+
key: Key::from(&secret_key.into()),
127
+
prm: Parameters {
128
+
kid: Some("at-oauth-00".to_string()),
129
+
cls: Some(Class::Signing),
130
+
..Default::default()
131
+
},
132
+
})
133
+
.expect("to get private key");
134
+
OAuthClient::new(OAuthClientConfig {
135
+
client_metadata: AtprotoClientMetadata {
136
+
client_id: format!("{base}/client-metadata.json"),
137
+
client_uri: Some(base.clone()),
138
+
redirect_uris: vec![format!("{base}/authorized")],
139
+
token_endpoint_auth_method: AuthMethod::PrivateKeyJwt,
140
+
grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
141
+
scopes: READONLY_SCOPE.to_vec(),
142
+
jwks_uri: Some(format!("{base}/.well-known/at-jwks.json")),
143
+
token_endpoint_auth_signing_alg: Some(String::from("ES256")),
144
+
},
145
+
keys: Some(vec![key]),
146
+
resolver,
147
+
state_store,
148
+
session_store,
149
+
})
150
+
.map_err(AuthSetupError::AtriumClientError)?
151
+
} else {
152
+
OAuthClient::new(OAuthClientConfig {
153
+
client_metadata: AtprotoLocalhostClientMetadata {
154
+
redirect_uris: Some(vec![String::from("http://127.0.0.1:9997/authorized")]),
155
+
scopes: Some(READONLY_SCOPE.to_vec()),
156
+
},
157
+
keys: None,
158
+
resolver,
159
+
state_store,
160
+
session_store,
161
+
})
162
+
.map_err(AuthSetupError::AtriumClientError)?
163
+
};
116
164
117
165
Ok(Self {
118
166
client: Arc::new(client),
119
167
did_resolver: Arc::new(did_resolver()),
120
168
})
169
+
}
170
+
171
+
pub fn client_metadata(&self) -> OAuthClientMetadata {
172
+
self.client.client_metadata.clone()
173
+
}
174
+
175
+
pub fn jwks(&self) -> JwkSet {
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
+22
-2
who-am-i/src/server.rs
···
1
1
use atrium_api::types::string::Did;
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
+
use jose_jwk::JwkSet;
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
+
#[allow(clippy::too_many_arguments)]
55
59
pub async fn serve(
56
60
shutdown: CancellationToken,
57
61
app_secret: String,
62
+
oauth_private_key: Option<PathBuf>,
58
63
tokens: Tokens,
64
+
base: String,
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
-
let oauth = OAuth::new().unwrap();
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
+
.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
+
.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
-
let listener = TcpListener::bind("0.0.0.0:9997")
106
+
eprintln!("starting server at http://{bind}");
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
+
}
311
+
312
+
async fn client_metadata(
313
+
State(AppState { oauth, .. }): State<AppState>,
314
+
) -> Json<OAuthClientMetadata> {
315
+
Json(oauth.client_metadata())
316
+
}
317
+
318
+
async fn at_jwks(State(AppState { oauth, .. }): State<AppState>) -> Json<JwkSet> {
319
+
Json(oauth.jwks())
300
320
}
301
321
302
322
#[derive(Debug, Deserialize)]