Skip to main content

smbcloud_auth/
oidc.rs

1use {
2    base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD},
3    reqwest::{Client, Url},
4    serde::{Deserialize, Serialize},
5    sha2::{Digest, Sha256},
6    smbcloud_model::error_codes::{ErrorCode, ErrorResponse},
7    smbcloud_network::{environment::Environment, network},
8    uuid::Uuid,
9};
10
11const AUTHORIZE_PATH: &str = "oauth/authorize";
12const TOKEN_PATH: &str = "oauth/token";
13const USERINFO_PATH: &str = "oauth/userinfo";
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct AuthorizationRequest {
17    pub authorize_url: String,
18    pub redirect_uri: String,
19    pub state: String,
20    pub code_verifier: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CallbackPayload {
25    pub code: String,
26    pub state: String,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct TokenResponse {
31    pub access_token: String,
32    pub token_type: String,
33    pub expires_in: Option<i32>,
34    pub refresh_token: Option<String>,
35    pub scope: Option<String>,
36    pub id_token: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct UserInfo {
41    pub sub: String,
42    pub email: Option<String>,
43    pub email_verified: Option<bool>,
44    pub tenant_id: Option<u64>,
45    pub tenant_slug: Option<String>,
46}
47
48pub fn build_authorization_request(
49    env: Environment,
50    oidc_client_id: &str,
51    redirect_uri: String,
52) -> Result<AuthorizationRequest, ErrorResponse> {
53    let code_verifier = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
54    let code_challenge = URL_SAFE_NO_PAD.encode(Sha256::digest(code_verifier.as_bytes()));
55    let state = Uuid::new_v4().to_string();
56
57    let mut url =
58        issuer_base_url(env)
59            .join(AUTHORIZE_PATH)
60            .map_err(|err| ErrorResponse::Error {
61                error_code: ErrorCode::ParseError,
62                message: err.to_string(),
63            })?;
64
65    url.query_pairs_mut()
66        .append_pair("client_id", oidc_client_id)
67        .append_pair("redirect_uri", &redirect_uri)
68        .append_pair("response_type", "code")
69        .append_pair("scope", "openid profile email")
70        .append_pair("state", &state)
71        .append_pair("code_challenge", &code_challenge)
72        .append_pair("code_challenge_method", "S256");
73
74    Ok(AuthorizationRequest {
75        authorize_url: url.to_string(),
76        redirect_uri,
77        state,
78        code_verifier,
79    })
80}
81
82pub fn parse_callback_url(callback_url: &str) -> Result<CallbackPayload, ErrorResponse> {
83    let url = Url::parse(callback_url).map_err(|err| ErrorResponse::Error {
84        error_code: ErrorCode::ParseError,
85        message: err.to_string(),
86    })?;
87
88    let mut code = None;
89    let mut state = None;
90
91    for (key, value) in url.query_pairs() {
92        match key.as_ref() {
93            "code" => code = Some(value.into_owned()),
94            "state" => state = Some(value.into_owned()),
95            _ => {}
96        }
97    }
98
99    match (code, state) {
100        (Some(code), Some(state)) => Ok(CallbackPayload { code, state }),
101        _ => Err(ErrorResponse::Error {
102            error_code: ErrorCode::InvalidParams,
103            message: "Missing authorization code or state.".to_string(),
104        }),
105    }
106}
107
108pub async fn exchange_code(
109    env: Environment,
110    oidc_client_id: &str,
111    redirect_uri: &str,
112    code: &str,
113    code_verifier: &str,
114) -> Result<TokenResponse, ErrorResponse> {
115    let url = issuer_base_url(env)
116        .join(TOKEN_PATH)
117        .map_err(|err| ErrorResponse::Error {
118            error_code: ErrorCode::ParseError,
119            message: err.to_string(),
120        })?;
121
122    let builder = Client::new()
123        .post(url)
124        .form(&[
125            ("grant_type", "authorization_code"),
126            ("client_id", oidc_client_id),
127            ("code", code),
128            ("redirect_uri", redirect_uri),
129            ("code_verifier", code_verifier),
130        ])
131        .header("Accept", "application/json");
132
133    network::request(builder).await
134}
135
136pub async fn get_userinfo(
137    env: Environment,
138    access_token: &str,
139    tenant_id: Option<&str>,
140) -> Result<UserInfo, ErrorResponse> {
141    let url = issuer_base_url(env)
142        .join(USERINFO_PATH)
143        .map_err(|err| ErrorResponse::Error {
144            error_code: ErrorCode::ParseError,
145            message: err.to_string(),
146        })?;
147
148    let mut builder = Client::new()
149        .get(url)
150        .bearer_auth(access_token)
151        .header("Accept", "application/json");
152
153    if let Some(tenant_id) = tenant_id {
154        builder = builder.header("X-Smbcloud-Tenant-Id", tenant_id);
155    }
156
157    network::request(builder).await
158}
159
160fn issuer_base_url(env: Environment) -> Url {
161    Url::parse(&format!("{}://{}/", env.api_protocol(), env.api_host()))
162        .expect("valid smbcloud api base url")
163}