Skip to content
This repository was archived by the owner on May 13, 2026. It is now read-only.

Commit 3c82040

Browse files
feat(user-management): add session (#185)
1 parent 3ae000a commit 3c82040

16 files changed

Lines changed: 1495 additions & 343 deletions

Cargo.lock

Lines changed: 393 additions & 326 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ native-tls = ["reqwest/native-tls"]
1313
rustls-tls = ["reqwest/rustls-tls"]
1414

1515
[dependencies]
16+
aead = { version = "0.5.2", features = ["std"] }
17+
aes-gcm = "0.10.3"
1618
async-trait = "0.1.88"
19+
base64 = "0.22.1"
1720
chrono = { version = "0.4.40", features = ["serde"] }
1821
derive_more = { version = "2.0.1", features = ["deref", "display", "from"] }
1922
jsonwebtoken = { version = "10.0.0", features = ["rust_crypto"] }
@@ -28,6 +31,7 @@ urlencoding = "2.1.3"
2831
[dev-dependencies]
2932
matches = "0.1.10"
3033
mockito = "1.0.0"
34+
rsa = "0.9.8"
3135
tokio = { version = "1.44.2", default-features = false, features = [
3236
"macros",
3337
"rt-multi-thread",

src/core/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod api_key;
22
mod metadata;
33
mod paginated_list;
44
mod pagination_params;
5+
mod remote_jwk_set;
56
mod timestamps;
67
mod unpaginated_list;
78
mod url_encodable_vec;
@@ -10,6 +11,7 @@ pub use api_key::*;
1011
pub use metadata::*;
1112
pub use paginated_list::*;
1213
pub use pagination_params::*;
14+
pub use remote_jwk_set::*;
1315
pub use timestamps::*;
1416
pub use unpaginated_list::*;
1517
pub(crate) use url_encodable_vec::*;

src/core/types/remote_jwk_set.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use std::sync::{Arc, Mutex};
2+
3+
use chrono::{DateTime, FixedOffset, TimeDelta, Utc};
4+
use jsonwebtoken::jwk::{Jwk, JwkSet};
5+
use thiserror::Error;
6+
use url::Url;
7+
8+
use crate::{ResponseExt, WorkOsError, WorkOsResult, user_management::GetJwksError};
9+
10+
type Entry = (JwkSet, DateTime<FixedOffset>);
11+
12+
/// An error returned from [`RemoteJwkSet::find`].
13+
#[derive(Debug, Error)]
14+
pub enum FindJwkError {
15+
/// Get JWKS error.
16+
#[error(transparent)]
17+
GetJwks(WorkOsError<GetJwksError>),
18+
19+
/// Poison error.
20+
#[error("poison error: {0}")]
21+
Poison(String),
22+
}
23+
24+
impl From<FindJwkError> for WorkOsError<FindJwkError> {
25+
fn from(value: FindJwkError) -> Self {
26+
Self::Operation(value)
27+
}
28+
}
29+
30+
/// Remote JSON Web Key Set (JWKS).
31+
#[derive(Clone)]
32+
pub struct RemoteJwkSet {
33+
client: reqwest::Client,
34+
url: Url,
35+
jwks: Arc<Mutex<Option<Entry>>>,
36+
}
37+
38+
impl RemoteJwkSet {
39+
pub(crate) fn new(client: reqwest::Client, url: Url) -> Self {
40+
RemoteJwkSet {
41+
client,
42+
url,
43+
jwks: Arc::new(Mutex::new(None)),
44+
}
45+
}
46+
47+
/// Find the key in the set that matches the given key id, if any.
48+
pub async fn find(&self, kid: &str) -> WorkOsResult<Option<Jwk>, FindJwkError> {
49+
{
50+
let jwks = self
51+
.jwks
52+
.lock()
53+
.map_err(|err| FindJwkError::Poison(err.to_string()))?;
54+
55+
if let Some((jwks, expires_at)) = jwks.as_ref()
56+
&& *expires_at > Utc::now().fixed_offset()
57+
{
58+
return Ok(jwks.find(kid).cloned());
59+
}
60+
}
61+
62+
let new_jwks = self
63+
.client
64+
.get(self.url.as_str())
65+
.send()
66+
.await?
67+
.handle_unauthorized_or_generic_error()
68+
.await?
69+
.json::<JwkSet>()
70+
.await?;
71+
72+
let key = new_jwks.find(kid).cloned();
73+
74+
// TODO: Consider using the expiry of keys instead?
75+
let mut jwks = self
76+
.jwks
77+
.lock()
78+
.map_err(|err| FindJwkError::Poison(err.to_string()))?;
79+
80+
*jwks = Some((new_jwks, Utc::now().fixed_offset() + TimeDelta::minutes(5)));
81+
82+
Ok(key)
83+
}
84+
}

src/sso/operations/get_authorization_url.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,12 @@ impl GetAuthorizationUrl for Sso<'_> {
105105
),
106106
};
107107

108+
let redirect_uri = urlencoding::encode(redirect_uri);
109+
108110
let mut query_params: querystring::QueryParams = vec![
109111
("response_type", "code"),
110112
("client_id", &client_id),
111-
("redirect_uri", redirect_uri),
113+
("redirect_uri", &redirect_uri),
112114
(connection_selector_param.0, &connection_selector_param.1),
113115
];
114116

@@ -149,7 +151,7 @@ mod test {
149151
assert_eq!(
150152
authorization_url,
151153
Url::parse(
152-
"https://api.workos.com/sso/authorize?response_type=code&client_id=client_123456789&redirect_uri=https://your-app.com/callback&connection=conn_1234"
154+
"https://api.workos.com/sso/authorize?response_type=code&client_id=client_123456789&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback&connection=conn_1234"
153155
)
154156
.unwrap()
155157
)
@@ -174,7 +176,7 @@ mod test {
174176
assert_eq!(
175177
authorization_url,
176178
Url::parse(
177-
"https://api.workos.com/sso/authorize?response_type=code&client_id=client_123456789&redirect_uri=https://your-app.com/callback&organization=org_1234"
179+
"https://api.workos.com/sso/authorize?response_type=code&client_id=client_123456789&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback&organization=org_1234"
178180
)
179181
.unwrap()
180182
)
@@ -197,7 +199,7 @@ mod test {
197199
assert_eq!(
198200
authorization_url,
199201
Url::parse(
200-
"https://api.workos.com/sso/authorize?response_type=code&client_id=client_123456789&redirect_uri=https://your-app.com/callback&provider=GoogleOAuth"
202+
"https://api.workos.com/sso/authorize?response_type=code&client_id=client_123456789&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback&provider=GoogleOAuth"
201203
)
202204
.unwrap()
203205
)

src/user_management.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,81 @@
22
//!
33
//! [WorkOS Docs: User Management](https://workos.com/docs/user-management)
44
5+
mod cookie_session;
56
mod operations;
67
mod types;
78

9+
use std::sync::{Arc, Mutex};
10+
11+
pub use cookie_session::*;
812
pub use operations::*;
13+
use thiserror::Error;
914
pub use types::*;
1015

11-
use crate::WorkOs;
16+
use crate::{RemoteJwkSet, WorkOs};
17+
18+
/// An error returned from [`UserManagement::jwks`].
19+
#[derive(Debug, Error)]
20+
pub enum JwksError {
21+
/// Missing client ID
22+
#[error("missing client ID")]
23+
MissingClientId,
24+
25+
/// Poison error.
26+
#[error("poison error: {0}")]
27+
Poison(String),
28+
29+
/// URL error.
30+
#[error(transparent)]
31+
Url(#[from] url::ParseError),
32+
}
1233

1334
/// User Management.
1435
///
1536
/// [WorkOS Docs: User Management](https://workos.com/docs/user-management)
1637
pub struct UserManagement<'a> {
1738
workos: &'a WorkOs,
39+
jwks: Arc<Mutex<Option<RemoteJwkSet>>>,
1840
}
1941

2042
impl<'a> UserManagement<'a> {
2143
/// Returns a new [`UserManagement`] instance for the provided WorkOS client.
2244
pub fn new(workos: &'a WorkOs) -> Self {
23-
Self { workos }
45+
Self {
46+
workos,
47+
jwks: Arc::new(Mutex::new(None)),
48+
}
49+
}
50+
51+
/// Get remote JSON Web Key Set (JWKS).
52+
pub fn jwks(&'a self) -> Result<RemoteJwkSet, JwksError> {
53+
let mut jwks = self
54+
.jwks
55+
.lock()
56+
.map_err(|err| JwksError::Poison(err.to_string()))?;
57+
58+
if let Some(jwks) = jwks.as_ref() {
59+
return Ok(jwks.clone());
60+
}
61+
62+
let Some(client_id) = self.workos.client_id() else {
63+
return Err(JwksError::MissingClientId);
64+
};
65+
66+
let new_jwks =
67+
RemoteJwkSet::new(self.workos.client().clone(), self.get_jwks_url(client_id)?);
68+
69+
*jwks = Some(new_jwks.clone());
70+
71+
Ok(new_jwks)
72+
}
73+
74+
/// Load the session by providing the sealed session and the cookie password.
75+
pub fn load_sealed_session(
76+
&'a self,
77+
session_data: &'a str,
78+
cookie_password: &'a str,
79+
) -> CookieSession<'a> {
80+
CookieSession::new(self, session_data, cookie_password)
2481
}
2582
}

0 commit comments

Comments
 (0)