Skip to main content

casdoor_rust_sdk/
mfa.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 std::collections::HashMap;
16
17use serde::{Deserialize, Serialize};
18
19use crate::client::{Client, PostBody};
20use crate::error::Result;
21use crate::serde_util::null_to_default;
22
23/// The MFA type sent by email.
24pub const MFA_TYPE_EMAIL: &str = "email";
25/// The MFA type sent by SMS.
26pub const MFA_TYPE_SMS: &str = "sms";
27/// The MFA type of an authenticator app (TOTP).
28pub const MFA_TYPE_APP: &str = "app";
29
30#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase", default)]
32pub struct MfaRequest {
33    pub owner: String,
34    pub mfa_type: String,
35    pub name: String,
36    #[serde(skip_serializing_if = "String::is_empty")]
37    pub secret: String,
38    #[serde(rename = "recoveryCodes", skip_serializing_if = "String::is_empty")]
39    pub recovery_code: String,
40}
41
42/// The data of the response of [`Client::initiate_mfa()`].
43#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase", default)]
45pub struct MfaInitiateData {
46    pub enabled: bool,
47    pub is_preferred: bool,
48    pub mfa_remember_in_hours: i32,
49    pub mfa_type: String,
50    #[serde(deserialize_with = "null_to_default")]
51    pub recovery_codes: Vec<String>,
52    pub secret: String,
53    pub url: String,
54}
55
56impl Client {
57    /// Start the setup of an MFA method for a user, the returned secret and URL are used
58    /// to show the QR code of the authenticator app.
59    pub async fn initiate_mfa(
60        &self,
61        owner: &str,
62        mfa_type: &str,
63        name: &str,
64    ) -> Result<MfaInitiateData> {
65        let request = MfaRequest {
66            owner: owner.to_string(),
67            mfa_type: mfa_type.to_string(),
68            name: name.to_string(),
69            ..Default::default()
70        };
71
72        let response = self
73            .do_post("mfa/setup/initiate", &[], to_form(&request)?)
74            .await?;
75
76        if response.data.is_null() {
77            return Ok(MfaInitiateData::default());
78        }
79
80        Ok(serde_json::from_value(response.data)?)
81    }
82
83    /// Verify the passcode of an MFA method that is being set up.
84    pub async fn verify_mfa(
85        &self,
86        owner: &str,
87        mfa_type: &str,
88        name: &str,
89        secret: &str,
90        passcode: &str,
91    ) -> Result<bool> {
92        let fields = HashMap::from([
93            ("owner".to_string(), owner.to_string()),
94            ("mfaType".to_string(), mfa_type.to_string()),
95            ("name".to_string(), name.to_string()),
96            ("secret".to_string(), secret.to_string()),
97            ("passcode".to_string(), passcode.to_string()),
98        ]);
99
100        let response = self
101            .do_post("mfa/setup/verify", &[], PostBody::Form(fields))
102            .await?;
103
104        Ok(response.status == "ok")
105    }
106
107    /// Enable a verified MFA method for a user.
108    pub async fn enable_mfa(
109        &self,
110        owner: &str,
111        mfa_type: &str,
112        name: &str,
113        secret: &str,
114        recovery_code: &str,
115    ) -> Result<bool> {
116        let request = MfaRequest {
117            owner: owner.to_string(),
118            mfa_type: mfa_type.to_string(),
119            name: name.to_string(),
120            secret: secret.to_string(),
121            recovery_code: recovery_code.to_string(),
122        };
123
124        let response = self
125            .do_post("mfa/setup/enable", &[], to_form(&request)?)
126            .await?;
127
128        Ok(response.status == "ok")
129    }
130
131    /// Set an MFA method as the preferred one of a user.
132    pub async fn set_preferred_mfa(
133        &self,
134        owner: &str,
135        mfa_type: &str,
136        name: &str,
137        secret: &str,
138    ) -> Result<()> {
139        let request = MfaRequest {
140            owner: owner.to_string(),
141            mfa_type: mfa_type.to_string(),
142            name: name.to_string(),
143            secret: secret.to_string(),
144            ..Default::default()
145        };
146
147        self.do_post("set-preferred-mfa", &[], to_form(&request)?)
148            .await?;
149
150        Ok(())
151    }
152
153    /// Delete all the MFA methods of a user.
154    pub async fn delete_mfa(&self, owner: &str, name: &str) -> Result<()> {
155        self.do_post(
156            "delete-mfa",
157            &[("owner", owner), ("name", name)],
158            PostBody::Raw(Vec::new()),
159        )
160        .await?;
161
162        Ok(())
163    }
164}
165
166/// The MFA APIs read their parameters from a form instead of from a JSON body.
167fn to_form(request: &MfaRequest) -> Result<PostBody<'static>> {
168    let fields = serde_json::from_value::<HashMap<String, String>>(serde_json::to_value(request)?)?;
169    Ok(PostBody::Form(fields))
170}