Skip to content

Commit 8471860

Browse files
committed
Merge branch 'feature/smbcloud-auth-sdk' into development
2 parents 86e85ba + 20b776f commit 8471860

20 files changed

Lines changed: 537 additions & 141 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ smbcloud-model = { version = "0.3", path = "crates/smbcloud-model" }
3131
smbcloud-network = { version = "0.3", path = "crates/smbcloud-network" }
3232
smbcloud-networking = { version = "0.3", path = "crates/smbcloud-networking" }
3333
smbcloud-auth = { version = "0.3", path = "crates/smbcloud-auth" }
34+
smbcloud-auth-sdk = { version = "0.3", path = "crates/smbcloud-auth-sdk" }
3435
smbcloud-networking-project = { version = "0.3", path = "crates/smbcloud-networking-project" }
3536
smbcloud-utils = { version = "0.3", path = "crates/smbcloud-utils" }
3637
spinners = "4.1.0"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "smbcloud-auth-sdk"
3+
version = "0.3.34"
4+
edition = "2024"
5+
authors = ["Seto Elkahfi <hej@setoelkahfi.se>"]
6+
description = "Client SDK for smbCloud Auth — authenticate your users via smbCloud Auth service."
7+
license = "Apache-2.0"
8+
repository = "https://github.com/smbcloudXYZ/smbcloud-cli"
9+
documentation = "https://smbcloud.xyz/posts"
10+
keywords = ["auth", "oidc", "authentication", "smbcloud", "sdk"]
11+
categories = ["authentication", "web-programming", "api-bindings"]
12+
readme = "README.md"
13+
14+
[dependencies]
15+
base64 = "0.22"
16+
reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots"] }
17+
serde = { workspace = true, features = ["derive"] }
18+
serde_json = { workspace = true }
19+
sha2 = "0.10"
20+
smbcloud-model = { workspace = true }
21+
smbcloud-network = { workspace = true }
22+
url-builder = { workspace = true }
23+
uuid = { version = "1", features = ["v4"] }

crates/smbcloud-auth-sdk/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# smbcloud-auth-sdk
2+
3+
Client SDK for [smbCloud Auth](https://smbcloud.xyz/) — lets you authenticate your users against the smbCloud Auth service using your app credentials (`client_id` / `client_secret`).
4+
5+
## Usage
6+
7+
```rust
8+
use smbcloud_auth_sdk::{
9+
client_credentials::ClientCredentials,
10+
login::login_with_client,
11+
signup::signup_with_client,
12+
me::me_with_client,
13+
};
14+
use smbcloud_network::environment::Environment;
15+
16+
let client = ClientCredentials {
17+
app_id: "your-app-id",
18+
app_secret: "your-app-secret",
19+
};
20+
21+
// Sign up
22+
let result = signup_with_client(Environment::Production, client, email, password).await?;
23+
24+
// Sign in
25+
let status = login_with_client(Environment::Production, client, email, password).await?;
26+
27+
// Profile
28+
let user = me_with_client(Environment::Production, client, &access_token).await?;
29+
```
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use {
2+
crate::client_credentials::{ClientCredentials, base_url_builder as tenant_base_url_builder},
3+
reqwest::Url,
4+
serde::{Deserialize, Serialize},
5+
smbcloud_model::error_codes::{ErrorCode, ErrorResponse},
6+
smbcloud_network::environment::Environment,
7+
uuid::Uuid,
8+
};
9+
10+
const TENANT_APPLE_AUTHORIZE_PATH: &str = "v1/client/oauth/apple/authorize";
11+
12+
#[derive(Debug, Clone, Serialize, Deserialize)]
13+
pub struct AppleAuthorizationRequest {
14+
pub authorize_url: String,
15+
pub redirect_uri: String,
16+
pub state: String,
17+
}
18+
19+
#[derive(Debug, Clone, Serialize, Deserialize)]
20+
pub struct AppleAuthSession {
21+
pub access_token: String,
22+
pub refresh_token: Option<String>,
23+
pub email: Option<String>,
24+
pub name: Option<String>,
25+
pub provider: String,
26+
pub provider_account_id: String,
27+
pub state: Option<String>,
28+
}
29+
30+
pub fn build_authorization_request_with_client(
31+
env: Environment,
32+
client: ClientCredentials<'_>,
33+
redirect_uri: String,
34+
state: Option<String>,
35+
) -> Result<AppleAuthorizationRequest, ErrorResponse> {
36+
let state = state.unwrap_or_else(|| Uuid::new_v4().to_string());
37+
let mut url_builder = tenant_base_url_builder(env, client);
38+
39+
url_builder
40+
.add_route(TENANT_APPLE_AUTHORIZE_PATH)
41+
.add_param("redirect_uri", &redirect_uri)
42+
.add_param("state", &state);
43+
44+
Ok(AppleAuthorizationRequest {
45+
authorize_url: url_builder.build(),
46+
redirect_uri,
47+
state,
48+
})
49+
}
50+
51+
pub fn parse_callback_url(
52+
callback_url: &str,
53+
expected_state: Option<&str>,
54+
) -> Result<AppleAuthSession, ErrorResponse> {
55+
let url = Url::parse(callback_url).map_err(|err| ErrorResponse::Error {
56+
error_code: ErrorCode::ParseError,
57+
message: err.to_string(),
58+
})?;
59+
60+
let mut access_token = None;
61+
let mut refresh_token = None;
62+
let mut email = None;
63+
let mut name = None;
64+
let mut provider = None;
65+
let mut provider_account_id = None;
66+
let mut state = None;
67+
let mut error = None;
68+
69+
for (key, value) in url.query_pairs() {
70+
match key.as_ref() {
71+
"access_token" => access_token = Some(value.into_owned()),
72+
"refresh_token" => refresh_token = Some(value.into_owned()),
73+
"email" => email = Some(value.into_owned()),
74+
"name" => name = Some(value.into_owned()),
75+
"provider" => provider = Some(value.into_owned()),
76+
"provider_account_id" => provider_account_id = Some(value.into_owned()),
77+
"state" => state = Some(value.into_owned()),
78+
"error" => error = Some(value.into_owned()),
79+
_ => {}
80+
}
81+
}
82+
83+
if let Some(error) = error {
84+
return Err(ErrorResponse::Error {
85+
error_code: ErrorCode::InvalidParams,
86+
message: error,
87+
});
88+
}
89+
90+
if let Some(expected_state) = expected_state {
91+
if state.as_deref() != Some(expected_state) {
92+
return Err(ErrorResponse::Error {
93+
error_code: ErrorCode::InvalidParams,
94+
message: "Apple callback state mismatch.".to_string(),
95+
});
96+
}
97+
}
98+
99+
Ok(AppleAuthSession {
100+
access_token: required_param("access_token", access_token)?,
101+
refresh_token,
102+
email,
103+
name,
104+
provider: provider.unwrap_or_else(|| "apple".to_string()),
105+
provider_account_id: required_param("provider_account_id", provider_account_id)?,
106+
state,
107+
})
108+
}
109+
110+
fn required_param(name: &str, value: Option<String>) -> Result<String, ErrorResponse> {
111+
value.ok_or_else(|| ErrorResponse::Error {
112+
error_code: ErrorCode::InvalidParams,
113+
message: format!("Missing `{name}` in Apple callback URL."),
114+
})
115+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use {smbcloud_network::environment::Environment, url_builder::URLBuilder};
2+
3+
#[derive(Clone, Copy)]
4+
pub struct ClientCredentials<'a> {
5+
pub app_id: &'a str,
6+
pub app_secret: &'a str,
7+
}
8+
9+
pub fn base_url_builder(env: Environment, client: ClientCredentials<'_>) -> URLBuilder {
10+
let mut url_builder = URLBuilder::new();
11+
url_builder
12+
.set_protocol(&env.api_protocol())
13+
.set_host(&env.api_host())
14+
.add_param("client_id", client.app_id)
15+
.add_param("client_secret", client.app_secret);
16+
url_builder
17+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pub mod apple;
2+
pub mod client_credentials;
3+
pub mod login;
4+
pub mod logout;
5+
pub mod me;
6+
pub mod oidc;
7+
pub mod remove;
8+
pub mod signup;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use {
2+
crate::client_credentials::{ClientCredentials, base_url_builder as tenant_base_url_builder},
3+
reqwest::Client,
4+
smbcloud_model::{
5+
error_codes::ErrorResponse,
6+
login::{AccountStatus, LoginParams, UserParam},
7+
},
8+
smbcloud_network::{environment::Environment, network::request_login},
9+
};
10+
11+
pub async fn login_with_client(
12+
env: Environment,
13+
client: ClientCredentials<'_>,
14+
username: String,
15+
password: String,
16+
) -> Result<AccountStatus, ErrorResponse> {
17+
let login_params = LoginParams {
18+
user: UserParam {
19+
email: username,
20+
password,
21+
},
22+
};
23+
let builder = Client::new()
24+
.post(build_login_url(env, client))
25+
.json(&login_params)
26+
.header("User-agent", client.app_id);
27+
request_login(builder).await
28+
}
29+
30+
fn build_login_url(env: Environment, client: ClientCredentials<'_>) -> String {
31+
let mut url_builder = tenant_base_url_builder(env, client);
32+
url_builder.add_route("v1/client/users/sign_in");
33+
url_builder.build()
34+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use {
2+
crate::client_credentials::{ClientCredentials, base_url_builder as tenant_base_url_builder},
3+
reqwest::{Client, StatusCode},
4+
smbcloud_model::error_codes::{ErrorCode, ErrorResponse},
5+
smbcloud_network::environment::Environment,
6+
};
7+
8+
pub async fn logout_with_client(
9+
env: Environment,
10+
client: ClientCredentials<'_>,
11+
access_token: String,
12+
) -> Result<(), ErrorResponse> {
13+
let response = match Client::new()
14+
.delete(build_logout_url(env, client))
15+
.header("Authorization", access_token)
16+
.header("Accept", "application/json")
17+
.header("Content-Type", "application/x-www-form-urlencoded")
18+
.send()
19+
.await
20+
{
21+
Ok(response) => response,
22+
Err(_) => {
23+
return Err(ErrorResponse::Error {
24+
error_code: ErrorCode::Unknown,
25+
message: ErrorCode::Unknown.message(None).to_string(),
26+
});
27+
}
28+
};
29+
30+
match response.status() {
31+
StatusCode::OK => Ok(()),
32+
_ => Err(ErrorResponse::Error {
33+
error_code: ErrorCode::Unauthorized,
34+
message: ErrorCode::Unauthorized.message(None).to_string(),
35+
}),
36+
}
37+
}
38+
39+
fn build_logout_url(env: Environment, client: ClientCredentials<'_>) -> String {
40+
let mut url_builder = tenant_base_url_builder(env, client);
41+
url_builder.add_route("v1/client/users/sign_out");
42+
url_builder.build()
43+
}

crates/smbcloud-auth-sdk/src/me.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use {
2+
crate::client_credentials::{ClientCredentials, base_url_builder as tenant_base_url_builder},
3+
reqwest::Client,
4+
smbcloud_model::{account::User, error_codes::ErrorResponse},
5+
smbcloud_network::{environment::Environment, network::request},
6+
};
7+
8+
pub async fn me_with_client(
9+
env: Environment,
10+
client: ClientCredentials<'_>,
11+
access_token: &str,
12+
) -> Result<User, ErrorResponse> {
13+
let builder = Client::new()
14+
.get(build_me_url(env, client))
15+
.header("Authorization", access_token)
16+
.header("Accept", "application/json")
17+
.header("Content-Type", "application/x-www-form-urlencoded");
18+
request(builder).await
19+
}
20+
21+
fn build_me_url(env: Environment, client: ClientCredentials<'_>) -> String {
22+
let mut url_builder = tenant_base_url_builder(env, client);
23+
url_builder.add_route("v1/client/me");
24+
url_builder.build()
25+
}

0 commit comments

Comments
 (0)