Skip to main content

casdoor_rust_sdk/
auth.rs

1// Copyright 2026 The Casdoor Authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use serde::{Deserialize, Serialize};
16
17use crate::client::Client;
18use crate::error::{CasdoorError, Result};
19
20/// The OAuth token returned by the Casdoor server.
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22pub struct OAuthToken {
23    #[serde(default)]
24    pub access_token: String,
25    #[serde(default)]
26    pub token_type: String,
27    #[serde(default)]
28    pub refresh_token: String,
29    #[serde(default)]
30    pub expires_in: i64,
31    #[serde(default)]
32    pub scope: String,
33    #[serde(default)]
34    pub id_token: String,
35    #[serde(default)]
36    pub error: String,
37    #[serde(default)]
38    pub error_description: String,
39}
40
41impl Client {
42    /// Get the pivotal and necessary secret to interact with the Casdoor server, by
43    /// exchanging the authorization code that Casdoor sent back to the redirect URI.
44    pub async fn get_oauth_token(&self, code: &str) -> Result<OAuthToken> {
45        self.request_oauth_token(
46            "access_token",
47            &[("grant_type", "authorization_code"), ("code", code)],
48        )
49        .await
50    }
51
52    /// Refresh the OAuth token with a refresh token.
53    pub async fn refresh_oauth_token(&self, refresh_token: &str) -> Result<OAuthToken> {
54        self.request_oauth_token(
55            "refresh_token",
56            &[
57                ("grant_type", "refresh_token"),
58                ("refresh_token", refresh_token),
59            ],
60        )
61        .await
62    }
63
64    /// Get the OAuth token via the `password` grant type, i.e. the "Resource Owner
65    /// Password Credentials Grant" of OAuth 2.0. The `password` grant type must be enabled
66    /// in the application's "Grant types" in Casdoor.
67    ///
68    /// The username is the user's name inside the application's organization, like
69    /// `alice` instead of `my-org/alice`.
70    pub async fn get_oauth_token_by_password(
71        &self,
72        username: &str,
73        password: &str,
74    ) -> Result<OAuthToken> {
75        self.request_oauth_token(
76            "access_token",
77            &[
78                ("grant_type", "password"),
79                ("username", username),
80                ("password", password),
81            ],
82        )
83        .await
84    }
85
86    /// Get an OAuth token which acts as the given user, so that an admin can call the APIs
87    /// on behalf of the user, without knowing the user's own password. It's the SDK
88    /// equivalent of the "Impersonation" button in Casdoor's Web UI.
89    ///
90    /// The `master_password` is the "Master password" of the user's organization, it needs
91    /// to be set in Casdoor first: "Organizations" -> Edit the organization -> "Master
92    /// password". See <https://casdoor.org/docs/user/impersonation>.
93    pub async fn impersonate_user(
94        &self,
95        username: &str,
96        master_password: &str,
97    ) -> Result<OAuthToken> {
98        self.get_oauth_token_by_password(username, master_password)
99            .await
100    }
101
102    /// Post the OAuth parameters to `/api/login/oauth/{token_action}`, the client ID and
103    /// the client secret are sent in the request body, as Casdoor expects.
104    async fn request_oauth_token(
105        &self,
106        token_action: &str,
107        params: &[(&str, &str)],
108    ) -> Result<OAuthToken> {
109        let url = format!("{}/api/login/oauth/{}", self.config.endpoint, token_action);
110
111        let mut form = params.to_vec();
112        form.push(("client_id", self.config.client_id.as_str()));
113        form.push(("client_secret", self.config.client_secret.as_str()));
114
115        let mut req = self.http().post(url).form(&form);
116        for (key, value) in &self.custom_headers {
117            req = req.header(key, value);
118        }
119
120        let response = req.send().await?;
121        let status = response.status();
122        let bytes = response.bytes().await?;
123
124        let token: OAuthToken = serde_json::from_slice(&bytes).map_err(|e| {
125            if status.is_success() {
126                CasdoorError::Json(e)
127            } else {
128                CasdoorError::HttpStatus {
129                    status: status.as_u16(),
130                    body: String::from_utf8_lossy(&bytes).into_owned(),
131                }
132            }
133        })?;
134
135        check_oauth_token(token)
136    }
137}
138
139/// Convert the `error: xxx` access token returned by the Casdoor server into a real error.
140fn check_oauth_token(token: OAuthToken) -> Result<OAuthToken> {
141    if !token.error.is_empty() {
142        let message = if token.error_description.is_empty() {
143            token.error
144        } else {
145            format!("{}: {}", token.error, token.error_description)
146        };
147
148        return Err(CasdoorError::Casdoor(message));
149    }
150
151    if let Some(message) = token.access_token.strip_prefix("error:") {
152        return Err(CasdoorError::Casdoor(message.trim().to_string()));
153    }
154
155    Ok(token)
156}