mz_frontegg_auth/
app_password.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::error::Error;
11use std::fmt;
12use std::str::FromStr;
13
14use base64::Engine;
15use base64::engine::general_purpose::URL_SAFE_NO_PAD;
16use base64::{
17    alphabet,
18    engine::{GeneralPurpose, GeneralPurposeConfig},
19};
20use serde::Deserialize;
21use uuid::Uuid;
22
23/// The prefix that identifies an app password as a Materialize password.
24pub const PREFIX: &str = "mzp_";
25
26/// A Materialize app password.
27///
28/// Somewhat unusually, the app password encodes both the client ID and secret
29/// for the API key in use. Both the client ID and secret are UUIDs. The
30/// password can have one of two formats:
31///
32///   * The URL-safe base64 encoding of the concatenated bytes of the UUIDs.
33///
34///     This format is a very compact representation (only 43 or 44 bytes)
35///     that is safe to use in a connection string without escaping.
36///
37///   * The concatenated hex-encoding of the UUIDs, with any number of
38///     special characters that are ignored.
39///
40///     This format allows for the UUIDs to be formatted with hyphens, or
41///     not.
42///
43#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct AppPassword {
45    /// The client ID embedded in the app password.
46    pub client_id: Uuid,
47    /// The secret key embedded in the app password.
48    pub secret_key: Uuid,
49}
50
51impl fmt::Display for AppPassword {
52    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
53        let mut buf = vec![];
54        buf.extend(self.client_id.as_bytes());
55        buf.extend(self.secret_key.as_bytes());
56        let encoded = URL_SAFE_NO_PAD.encode(buf);
57        f.write_str(PREFIX)?;
58        f.write_str(&encoded)
59    }
60}
61
62impl FromStr for AppPassword {
63    type Err = AppPasswordParseError;
64
65    fn from_str(password: &str) -> Result<AppPassword, AppPasswordParseError> {
66        let password = password.strip_prefix(PREFIX).ok_or(AppPasswordParseError)?;
67        if password.len() == 43 || password.len() == 44 {
68            // If it's exactly 43 or 44 bytes, assume we have base64-encoded
69            // UUID bytes without or with padding, respectively.
70            let url_safe_engine = GeneralPurpose::new(
71                &alphabet::URL_SAFE,
72                GeneralPurposeConfig::new()
73                    .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
74            );
75            let buf = url_safe_engine
76                .decode(password)
77                .map_err(|_| AppPasswordParseError)?;
78            let client_id = Uuid::from_slice(&buf[..16]).map_err(|_| AppPasswordParseError)?;
79            let secret_key = Uuid::from_slice(&buf[16..]).map_err(|_| AppPasswordParseError)?;
80            Ok(AppPassword {
81                client_id,
82                secret_key,
83            })
84        } else if password.len() >= 64 {
85            // If it's more than 64 bytes, assume we have concatenated
86            // hex-encoded UUIDs, possibly with some special characters mixed
87            // in.
88            let mut chars = password.chars().filter(|c| c.is_alphanumeric());
89            let client_id = Uuid::parse_str(&chars.by_ref().take(32).collect::<String>())
90                .map_err(|_| AppPasswordParseError)?;
91            let secret_key = Uuid::parse_str(&chars.take(32).collect::<String>())
92                .map_err(|_| AppPasswordParseError)?;
93            Ok(AppPassword {
94                client_id,
95                secret_key,
96            })
97        } else {
98            // Otherwise it's definitely not a password format we understand.
99            Err(AppPasswordParseError)
100        }
101    }
102}
103
104/// An error while parsing an [`AppPassword`].
105#[derive(Clone, Debug)]
106pub struct AppPasswordParseError;
107
108impl Error for AppPasswordParseError {}
109
110impl fmt::Display for AppPasswordParseError {
111    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
112        f.write_str("invalid app password format")
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use uuid::Uuid;
119
120    use super::AppPassword;
121
122    #[mz_ore::test]
123    fn test_app_password() {
124        struct TestCase {
125            input: &'static str,
126            expected_output: &'static str,
127            expected_client_id: Uuid,
128            expected_secret_key: Uuid,
129        }
130
131        for tc in [
132            TestCase {
133                input: "mzp_7ce3c1e8ea854594ad5d785f17d1736f1947fdcef5404adb84a47347e5d30c9f",
134                expected_output: "mzp_fOPB6OqFRZStXXhfF9FzbxlH_c71QErbhKRzR-XTDJ8",
135                expected_client_id: "7ce3c1e8-ea85-4594-ad5d-785f17d1736f".parse().unwrap(),
136                expected_secret_key: "1947fdce-f540-4adb-84a4-7347e5d30c9f".parse().unwrap(),
137            },
138            TestCase {
139                input: "mzp_fOPB6OqFRZStXXhfF9FzbxlH_c71QErbhKRzR-XTDJ8",
140                expected_output: "mzp_fOPB6OqFRZStXXhfF9FzbxlH_c71QErbhKRzR-XTDJ8",
141                expected_client_id: "7ce3c1e8-ea85-4594-ad5d-785f17d1736f".parse().unwrap(),
142                expected_secret_key: "1947fdce-f540-4adb-84a4-7347e5d30c9f".parse().unwrap(),
143            },
144            TestCase {
145                input: "mzp_0445db36-5826-41af-84f6-e09402fc6171:a0c11434-07ba-426a-b83d-cc4f192325a3",
146                expected_output: "mzp_BEXbNlgmQa-E9uCUAvxhcaDBFDQHukJquD3MTxkjJaM",
147                expected_client_id: "0445db36-5826-41af-84f6-e09402fc6171".parse().unwrap(),
148                expected_secret_key: "a0c11434-07ba-426a-b83d-cc4f192325a3".parse().unwrap(),
149            },
150        ] {
151            let app_password: AppPassword = tc.input.parse().unwrap();
152            assert_eq!(app_password.to_string(), tc.expected_output);
153            assert_eq!(app_password.client_id, tc.expected_client_id);
154            assert_eq!(app_password.secret_key, tc.expected_secret_key);
155        }
156    }
157}