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

Commit 6a69a6c

Browse files
feat(user-management): add authenticate with organization selection (#187)
1 parent 22812db commit 6a69a6c

4 files changed

Lines changed: 335 additions & 2 deletions

File tree

src/user_management/operations.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod authenticate_with_code;
33
mod authenticate_with_device_code;
44
mod authenticate_with_email_verification;
55
mod authenticate_with_magic_auth;
6+
mod authenticate_with_organization_selection;
67
mod authenticate_with_password;
78
mod authenticate_with_refresh_token;
89
mod authenticate_with_totp;
@@ -46,6 +47,7 @@ pub use authenticate_with_code::*;
4647
pub use authenticate_with_device_code::*;
4748
pub use authenticate_with_email_verification::*;
4849
pub use authenticate_with_magic_auth::*;
50+
pub use authenticate_with_organization_selection::*;
4951
pub use authenticate_with_password::*;
5052
pub use authenticate_with_refresh_token::*;
5153
pub use authenticate_with_totp::*;

src/user_management/operations/authenticate_with_email_verification.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ mod test {
315315
"The code '123456' has expired or is invalid."
316316
);
317317
} else {
318-
panic!("expected authenticate_with_magic_auth to return an error")
318+
panic!("expected authenticate_with_email_verification to return an error")
319319
}
320320
}
321321
}
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
use std::net::IpAddr;
2+
3+
use async_trait::async_trait;
4+
use serde::Serialize;
5+
6+
use crate::organizations::OrganizationId;
7+
use crate::sso::ClientId;
8+
use crate::user_management::{
9+
AuthenticateError, AuthenticationResponse, HandleAuthenticateError, PendingAuthenticationToken,
10+
UserManagement,
11+
};
12+
use crate::{ApiKey, WorkOsResult};
13+
14+
/// The parameters for [`AuthenticateWithOrganizationSelection`].
15+
#[derive(Debug, Serialize)]
16+
pub struct AuthenticateWithOrganizationSelectionParams<'a> {
17+
/// Identifies the application making the request to the WorkOS server.
18+
pub client_id: &'a ClientId,
19+
20+
/// The authentication token returned from a failed authentication attempt due to the corresponding error.
21+
pub pending_authentication_token: &'a PendingAuthenticationToken,
22+
23+
/// The organization the user selected to sign in to.
24+
pub organization_id: &'a OrganizationId,
25+
26+
/// The IP address of the request from the user who is attempting to authenticate.
27+
pub ip_address: Option<&'a IpAddr>,
28+
29+
/// The user agent of the request from the user who is attempting to authenticate.
30+
pub user_agent: Option<&'a str>,
31+
}
32+
33+
#[derive(Serialize)]
34+
struct AuthenticateWithOrganizationSelectionBody<'a> {
35+
/// Authenticates the application making the request to the WorkOS server.
36+
client_secret: &'a ApiKey,
37+
38+
/// A string constant that distinguishes the method by which your application will receive an access token.
39+
grant_type: &'a str,
40+
41+
#[serde(flatten)]
42+
params: &'a AuthenticateWithOrganizationSelectionParams<'a>,
43+
}
44+
45+
/// [WorkOS Docs: Authenticate with organization selection](https://workos.com/docs/reference/user-management/authentication/organization-selection)
46+
#[async_trait]
47+
pub trait AuthenticateWithOrganizationSelection {
48+
/// Authenticates a user into an organization they are a member of.
49+
///
50+
/// [WorkOS Docs: Authenticate with organization selection](https://workos.com/docs/reference/user-management/authentication/organization-selection)
51+
///
52+
/// # Examples
53+
///
54+
/// ```
55+
/// # use std::{net::IpAddr, str::FromStr};
56+
///
57+
/// # use workos::WorkOsResult;
58+
/// # use workos::organizations::OrganizationId;
59+
/// # use workos::sso::ClientId;
60+
/// # use workos::user_management::*;
61+
/// use workos::{ApiKey, WorkOs};
62+
///
63+
/// # async fn run() -> WorkOsResult<(), AuthenticateError> {
64+
/// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789"));
65+
///
66+
/// let AuthenticationResponse { user, .. } = workos
67+
/// .user_management()
68+
/// .authenticate_with_organization_selection(&AuthenticateWithOrganizationSelectionParams {
69+
/// client_id: &ClientId::from("client_123456789"),
70+
/// pending_authentication_token: &PendingAuthenticationToken::from("ql1AJgNoLN1tb9llaQ8jyC2dn"),
71+
/// organization_id: &OrganizationId::from("org_01H93Z2SYX1D3NJ536M94T8SHP"),
72+
/// ip_address: Some(&IpAddr::from_str("192.0.2.1")?),
73+
/// user_agent: Some("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"),
74+
/// })
75+
/// .await?;
76+
/// # Ok(())
77+
/// # }
78+
/// ```
79+
async fn authenticate_with_organization_selection(
80+
&self,
81+
params: &AuthenticateWithOrganizationSelectionParams<'_>,
82+
) -> WorkOsResult<AuthenticationResponse, AuthenticateError>;
83+
}
84+
85+
#[async_trait]
86+
impl AuthenticateWithOrganizationSelection for UserManagement<'_> {
87+
async fn authenticate_with_organization_selection(
88+
&self,
89+
params: &AuthenticateWithOrganizationSelectionParams<'_>,
90+
) -> WorkOsResult<AuthenticationResponse, AuthenticateError> {
91+
let url = self
92+
.workos
93+
.base_url()
94+
.join("/user_management/authenticate")?;
95+
96+
let body = AuthenticateWithOrganizationSelectionBody {
97+
client_secret: self.workos.key(),
98+
grant_type: "urn:workos:oauth:grant-type:organization-selection",
99+
params,
100+
};
101+
102+
let authenticate_with_organization_selection_response = self
103+
.workos
104+
.client()
105+
.post(url)
106+
.json(&body)
107+
.send()
108+
.await?
109+
.handle_authenticate_error()
110+
.await?
111+
.json::<AuthenticationResponse>()
112+
.await?;
113+
114+
Ok(authenticate_with_organization_selection_response)
115+
}
116+
}
117+
118+
#[cfg(test)]
119+
mod test {
120+
use matches::assert_matches;
121+
use mockito::Matcher;
122+
use serde_json::json;
123+
use tokio;
124+
125+
use crate::sso::AccessToken;
126+
use crate::user_management::{RefreshToken, UserId};
127+
use crate::{ApiKey, WorkOs, WorkOsError};
128+
129+
use super::*;
130+
131+
#[tokio::test]
132+
async fn it_calls_the_token_endpoint() {
133+
let mut server = mockito::Server::new_async().await;
134+
135+
let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789"))
136+
.base_url(&server.url())
137+
.unwrap()
138+
.build();
139+
140+
server
141+
.mock("POST", "/user_management/authenticate")
142+
.match_body(Matcher::PartialJson(json!({
143+
"client_id": "client_123456789",
144+
"client_secret": "sk_example_123456789",
145+
"grant_type": "urn:workos:oauth:grant-type:organization-selection",
146+
"pending_authentication_token": "ql1AJgNoLN1tb9llaQ8jyC2dn",
147+
"organization_id": "org_01H93Z2SYX1D3NJ536M94T8SHP"
148+
})))
149+
.with_status(200)
150+
.with_body(
151+
json!({
152+
"user": {
153+
"object": "user",
154+
"id": "user_01E4ZCR3C56J083X43JQXF3JK5",
155+
"email": "marcelina.davis@example.com",
156+
"first_name": "Marcelina",
157+
"last_name": "Davis",
158+
"email_verified": true,
159+
"profile_picture_url": "https://workoscdn.com/images/v1/123abc",
160+
"metadata": {},
161+
"created_at": "2021-06-25T19:07:33.155Z",
162+
"updated_at": "2021-06-25T19:07:33.155Z"
163+
},
164+
"organization_id": "org_01H945H0YD4F97JN9MATX7BYAG",
165+
"access_token": "eyJhb.nNzb19vaWRjX2tleV9.lc5Uk4yWVk5In0",
166+
"refresh_token": "yAjhKk123NLIjdrBdGZPf8pLIDvK",
167+
"authentication_method": "Password"
168+
})
169+
.to_string(),
170+
)
171+
.create_async()
172+
.await;
173+
174+
let response = workos
175+
.user_management()
176+
.authenticate_with_organization_selection(
177+
&AuthenticateWithOrganizationSelectionParams {
178+
client_id: &ClientId::from("client_123456789"),
179+
pending_authentication_token: &PendingAuthenticationToken::from(
180+
"ql1AJgNoLN1tb9llaQ8jyC2dn",
181+
),
182+
organization_id: &OrganizationId::from("org_01H93Z2SYX1D3NJ536M94T8SHP"),
183+
ip_address: None,
184+
user_agent: None,
185+
},
186+
)
187+
.await
188+
.unwrap();
189+
190+
assert_eq!(
191+
response.access_token,
192+
AccessToken::from("eyJhb.nNzb19vaWRjX2tleV9.lc5Uk4yWVk5In0")
193+
);
194+
assert_eq!(
195+
response.refresh_token,
196+
RefreshToken::from("yAjhKk123NLIjdrBdGZPf8pLIDvK")
197+
);
198+
assert_eq!(
199+
response.user.id,
200+
UserId::from("user_01E4ZCR3C56J083X43JQXF3JK5")
201+
)
202+
}
203+
204+
#[tokio::test]
205+
async fn it_returns_an_unauthorized_error_with_an_invalid_client() {
206+
let mut server = mockito::Server::new_async().await;
207+
208+
let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789"))
209+
.base_url(&server.url())
210+
.unwrap()
211+
.build();
212+
213+
server
214+
.mock("POST", "/user_management/authenticate")
215+
.with_status(400)
216+
.with_body(
217+
json!({
218+
"error": "invalid_client",
219+
"error_description": "Invalid client ID."
220+
})
221+
.to_string(),
222+
)
223+
.create_async()
224+
.await;
225+
226+
let result = workos
227+
.user_management()
228+
.authenticate_with_organization_selection(
229+
&AuthenticateWithOrganizationSelectionParams {
230+
client_id: &ClientId::from("client_123456789"),
231+
pending_authentication_token: &PendingAuthenticationToken::from(
232+
"ql1AJgNoLN1tb9llaQ8jyC2dn",
233+
),
234+
organization_id: &OrganizationId::from("org_01H93Z2SYX1D3NJ536M94T8SHP"),
235+
ip_address: None,
236+
user_agent: None,
237+
},
238+
)
239+
.await;
240+
241+
assert_matches!(result, Err(WorkOsError::Unauthorized))
242+
}
243+
244+
#[tokio::test]
245+
async fn it_returns_an_unauthorized_error_with_an_unauthorized_client() {
246+
let mut server = mockito::Server::new_async().await;
247+
248+
let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789"))
249+
.base_url(&server.url())
250+
.unwrap()
251+
.build();
252+
253+
server
254+
.mock("POST", "/user_management/authenticate")
255+
.with_status(400)
256+
.with_body(
257+
json!({
258+
"error": "unauthorized_client",
259+
"error_description": "Unauthorized"
260+
})
261+
.to_string(),
262+
)
263+
.create_async()
264+
.await;
265+
266+
let result = workos
267+
.user_management()
268+
.authenticate_with_organization_selection(
269+
&AuthenticateWithOrganizationSelectionParams {
270+
client_id: &ClientId::from("client_123456789"),
271+
pending_authentication_token: &PendingAuthenticationToken::from(
272+
"ql1AJgNoLN1tb9llaQ8jyC2dn",
273+
),
274+
organization_id: &OrganizationId::from("org_01H93Z2SYX1D3NJ536M94T8SHP"),
275+
ip_address: None,
276+
user_agent: None,
277+
},
278+
)
279+
.await;
280+
281+
assert_matches!(result, Err(WorkOsError::Unauthorized))
282+
}
283+
284+
#[tokio::test]
285+
async fn it_returns_an_error_when_the_authorization_code_is_invalid() {
286+
let mut server = mockito::Server::new_async().await;
287+
288+
let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789"))
289+
.base_url(&server.url())
290+
.unwrap()
291+
.build();
292+
293+
server
294+
.mock("POST", "/user_management/authenticate")
295+
.with_status(400)
296+
.with_body(
297+
json!({
298+
"error": "invalid_grant",
299+
"error_description": "The code '123456' has expired or is invalid."
300+
})
301+
.to_string(),
302+
)
303+
.create_async()
304+
.await;
305+
306+
let result = workos
307+
.user_management()
308+
.authenticate_with_organization_selection(
309+
&AuthenticateWithOrganizationSelectionParams {
310+
client_id: &ClientId::from("client_123456789"),
311+
pending_authentication_token: &PendingAuthenticationToken::from(
312+
"ql1AJgNoLN1tb9llaQ8jyC2dn",
313+
),
314+
organization_id: &OrganizationId::from("org_01H93Z2SYX1D3NJ536M94T8SHP"),
315+
ip_address: None,
316+
user_agent: None,
317+
},
318+
)
319+
.await;
320+
321+
if let Err(WorkOsError::Operation(AuthenticateError::WithError(error))) = result {
322+
assert_eq!(error.error(), "invalid_grant");
323+
assert_eq!(
324+
error.error_description(),
325+
"The code '123456' has expired or is invalid."
326+
);
327+
} else {
328+
panic!("expected authenticate_with_organization_selection to return an error")
329+
}
330+
}
331+
}

src/user_management/operations/authenticate_with_totp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ mod test {
334334
"The code '123456' has expired or is invalid."
335335
);
336336
} else {
337-
panic!("expected authenticate_with_magic_auth to return an error")
337+
panic!("expected authenticate_with_totp to return an error")
338338
}
339339
}
340340
}

0 commit comments

Comments
 (0)