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

Commit ac3af62

Browse files
feat(user-management): add authenticate with email verification (#36)
1 parent d38e825 commit ac3af62

7 files changed

Lines changed: 337 additions & 2 deletions

src/user_management/operations.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod authenticate_with_code;
2+
mod authenticate_with_email_verification;
23
mod authenticate_with_magic_auth;
34
mod authenticate_with_password;
45
mod authenticate_with_refresh_token;
@@ -13,6 +14,7 @@ mod get_user_identities;
1314
mod list_users;
1415

1516
pub use authenticate_with_code::*;
17+
pub use authenticate_with_email_verification::*;
1618
pub use authenticate_with_magic_auth::*;
1719
pub use authenticate_with_password::*;
1820
pub use authenticate_with_refresh_token::*;

src/user_management/operations/authenticate_with_code.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ pub struct AuthenticateWithCodeParams<'a> {
3333

3434
#[derive(Serialize)]
3535
struct AuthenticateWithCodeBody<'a> {
36+
/// Authenticates the application making the request to the WorkOS server.
3637
client_secret: &'a ApiKey,
38+
39+
/// A string constant that distinguishes the method by which your application will receive an access token.
3740
grant_type: &'a str,
3841

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

src/user_management/operations/authenticate_with_magic_auth.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ pub struct AuthenticateWithMagicAuthParams<'a> {
3131

3232
#[derive(Serialize)]
3333
struct AuthenticateWithMagicAuthBody<'a> {
34+
/// Authenticates the application making the request to the WorkOS server.
3435
client_secret: &'a ApiKey,
36+
37+
/// A string constant that distinguishes the method by which your application will receive an access token.
3538
grant_type: &'a str,
3639

3740
#[serde(flatten)]
@@ -41,7 +44,7 @@ struct AuthenticateWithMagicAuthBody<'a> {
4144
/// [WorkOS Docs: Authenticate with Magic Auth](https://workos.com/docs/reference/user-management/authentication/magic-auth)
4245
#[async_trait]
4346
pub trait AuthenticateWithMagicAuth {
44-
/// Authenticates a user by verifying the Magic Auth code sent to the users email.
47+
/// Authenticates a user by verifying the Magic Auth code sent to the user's email.
4548
///
4649
/// [WorkOS Docs: Authenticate with Magic Auth](https://workos.com/docs/reference/user-management/authentication/magic-auth)
4750
///

src/user_management/operations/authenticate_with_password.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ pub struct AuthenticateWithPasswordParams<'a> {
3333

3434
#[derive(Serialize)]
3535
struct AuthenticateWithPasswordBody<'a> {
36+
/// Authenticates the application making the request to the WorkOS server.
3637
client_secret: &'a ApiKey,
38+
39+
/// A string constant that distinguishes the method by which your application will receive an access token.
3740
grant_type: &'a str,
3841

3942
#[serde(flatten)]

src/user_management/operations/authenticate_with_refresh_token.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ pub struct AuthenticateWithRefreshTokenParams<'a> {
3232

3333
#[derive(Serialize)]
3434
struct AuthenticateWithRefreshTokenBody<'a> {
35+
/// Authenticates the application making the request to the WorkOS server.
3536
client_secret: &'a ApiKey,
37+
38+
/// A string constant that distinguishes the method by which your application will receive an access token.
3639
grant_type: &'a str,
3740

3841
#[serde(flatten)]

src/user_management/operations/create_user.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub struct CreateUserParams<'a> {
2121
/// The last name of the user.
2222
pub last_name: Option<&'a str>,
2323

24-
/// Whether the users email address was previously verified.
24+
/// Whether the user's email address was previously verified.
2525
pub email_verified: Option<bool>,
2626

2727
/// The external ID of the user.

0 commit comments

Comments
 (0)