onshape_client_core/
auth.rs1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
8use oauth2::AccessToken;
9use schemars::JsonSchema;
10use secrecy::{ExposeSecret, SecretString};
11use serde::{Deserialize, Serialize};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
22#[serde(rename_all = "snake_case")]
23#[non_exhaustive]
24pub enum AuthMethod {
25 Auto,
33 Basic,
38 #[serde(rename = "oauth")]
44 OAuth,
45 }
50
51pub struct Credentials {
53 pub access_key: SecretString,
55 pub secret_key: SecretString,
57}
58
59#[must_use]
88pub fn basic_authorization_header_value(credentials: &Credentials) -> SecretString {
89 let access = credentials.access_key.expose_secret();
90 let secret = credentials.secret_key.expose_secret();
91 let encoded = BASE64.encode(format!("{access}:{secret}"));
92 SecretString::from(format!("Basic {encoded}"))
93}
94
95#[must_use]
106pub fn bearer_authorization_header_value(access_token: &AccessToken) -> SecretString {
107 SecretString::from(format!("Bearer {}", access_token.secret()))
108}
109
110#[cfg(test)]
115#[allow(clippy::expect_used)]
116mod tests {
117 use super::*;
118
119 fn test_credentials() -> Credentials {
120 Credentials {
121 access_key: SecretString::from("my_access_key"),
122 secret_key: SecretString::from("my_secret_key"),
123 }
124 }
125
126 #[test]
127 fn basic_auth_starts_with_basic_prefix() {
128 let creds = test_credentials();
129 let header = basic_authorization_header_value(&creds);
130 assert!(header.expose_secret().starts_with("Basic "));
131 }
132
133 #[test]
134 fn basic_auth_encodes_correctly() {
135 let creds = test_credentials();
136 let header = basic_authorization_header_value(&creds);
137 let value = header.expose_secret();
138
139 let encoded = value
141 .strip_prefix("Basic ")
142 .expect("should have Basic prefix");
143 let decoded_bytes = BASE64.decode(encoded).expect("should be valid base64");
144 let decoded = String::from_utf8(decoded_bytes).expect("should be valid UTF-8");
145
146 assert_eq!(decoded, "my_access_key:my_secret_key");
147 }
148
149 #[test]
150 fn basic_auth_matches_known_value() {
151 let creds = Credentials {
154 access_key: SecretString::from("access"),
155 secret_key: SecretString::from("secret"),
156 };
157 let header = basic_authorization_header_value(&creds);
158 assert_eq!(header.expose_secret(), "Basic YWNjZXNzOnNlY3JldA==");
159 }
160
161 #[test]
162 fn basic_auth_handles_empty_keys() {
163 let creds = Credentials {
164 access_key: SecretString::from(""),
165 secret_key: SecretString::from(""),
166 };
167 let header = basic_authorization_header_value(&creds);
168 let value = header.expose_secret();
169
170 let encoded = value
171 .strip_prefix("Basic ")
172 .expect("should have Basic prefix");
173 let decoded_bytes = BASE64.decode(encoded).expect("should be valid base64");
174 let decoded = String::from_utf8(decoded_bytes).expect("should be valid UTF-8");
175
176 assert_eq!(decoded, ":");
177 }
178
179 #[test]
180 fn basic_auth_handles_special_characters() {
181 let creds = Credentials {
182 access_key: SecretString::from("key+with/special=chars"),
183 secret_key: SecretString::from("s3cr3t!@#$%^&*()"),
184 };
185 let header = basic_authorization_header_value(&creds);
186 let value = header.expose_secret();
187
188 let encoded = value
189 .strip_prefix("Basic ")
190 .expect("should have Basic prefix");
191 let decoded_bytes = BASE64.decode(encoded).expect("should be valid base64");
192 let decoded = String::from_utf8(decoded_bytes).expect("should be valid UTF-8");
193
194 assert_eq!(decoded, "key+with/special=chars:s3cr3t!@#$%^&*()");
195 }
196
197 #[test]
198 fn basic_auth_handles_colon_in_keys() {
199 let creds = Credentials {
202 access_key: SecretString::from("key:with:colons"),
203 secret_key: SecretString::from("secret:too"),
204 };
205 let header = basic_authorization_header_value(&creds);
206 let value = header.expose_secret();
207
208 let encoded = value
209 .strip_prefix("Basic ")
210 .expect("should have Basic prefix");
211 let decoded_bytes = BASE64.decode(encoded).expect("should be valid base64");
212 let decoded = String::from_utf8(decoded_bytes).expect("should be valid UTF-8");
213
214 assert_eq!(decoded, "key:with:colons:secret:too");
215 }
216
217 #[test]
218 fn auth_method_serializes_to_snake_case() {
219 let json = serde_json::to_string(&AuthMethod::Basic).expect("should serialize");
220 assert_eq!(json, "\"basic\"");
221 }
222
223 #[test]
224 fn auth_method_deserializes_from_snake_case() {
225 let method: AuthMethod = serde_json::from_str("\"basic\"").expect("should deserialize");
226 assert_eq!(method, AuthMethod::Basic);
227 }
228
229 #[test]
230 fn auth_method_oauth_serializes_to_snake_case() {
231 let json = serde_json::to_string(&AuthMethod::OAuth).expect("should serialize");
232 assert_eq!(json, "\"oauth\"");
233 }
234
235 #[test]
236 fn auth_method_oauth_deserializes_from_snake_case() {
237 let method: AuthMethod = serde_json::from_str("\"oauth\"").expect("should deserialize");
238 assert_eq!(method, AuthMethod::OAuth);
239 }
240
241 #[test]
242 fn auth_method_auto_serializes_to_snake_case() {
243 let json = serde_json::to_string(&AuthMethod::Auto).expect("should serialize");
244 assert_eq!(json, "\"auto\"");
245 }
246
247 #[test]
248 fn auth_method_auto_deserializes_from_snake_case() {
249 let method: AuthMethod = serde_json::from_str("\"auto\"").expect("should deserialize");
250 assert_eq!(method, AuthMethod::Auto);
251 }
252
253 #[test]
254 fn bearer_auth_starts_with_bearer_prefix() {
255 let token = AccessToken::new("test-access-token".to_string());
256 let header = bearer_authorization_header_value(&token);
257 assert!(header.expose_secret().starts_with("Bearer "));
258 }
259
260 #[test]
261 fn bearer_auth_contains_token() {
262 let token = AccessToken::new("my-oauth-token-12345".to_string());
263 let header = bearer_authorization_header_value(&token);
264 assert_eq!(header.expose_secret(), "Bearer my-oauth-token-12345");
265 }
266
267 #[test]
268 fn bearer_auth_handles_empty_token() {
269 let token = AccessToken::new(String::new());
270 let header = bearer_authorization_header_value(&token);
271 assert_eq!(header.expose_secret(), "Bearer ");
272 }
273}