reifydb_auth/method/
github.rs1use std::collections::HashMap;
5
6use reifydb_core::interface::auth::{AuthStep, AuthenticationProvider};
7use reifydb_runtime::context::rng::Rng;
8use reifydb_value::{Result, error::Error};
9use subtle::ConstantTimeEq;
10
11use crate::error::GithubError;
12
13pub struct GithubProvider;
14
15impl AuthenticationProvider for GithubProvider {
16 fn method(&self) -> &str {
17 "github"
18 }
19
20 fn create(&self, _rng: &Rng, config: &HashMap<String, String>) -> Result<HashMap<String, String>> {
21 let user_id = config.get("user_id").ok_or_else(|| Error::from(GithubError::MissingUserId))?;
22
23 if user_id.is_empty() || !user_id.bytes().all(|b| b.is_ascii_digit()) {
24 return Err(Error::from(GithubError::InvalidUserId {
25 reason: "expected the numeric github account id".to_string(),
26 }));
27 }
28
29 let mut properties = HashMap::from([("user_id".to_string(), user_id.clone())]);
30 if let Some(login) = config.get("login") {
31 properties.insert("login".to_string(), login.clone());
32 }
33 Ok(properties)
34 }
35
36 fn authenticate(
37 &self,
38 stored: &HashMap<String, String>,
39 credentials: &HashMap<String, String>,
40 ) -> Result<AuthStep> {
41 let stored_id = stored.get("user_id").ok_or_else(|| Error::from(GithubError::MissingUserId))?;
42
43 let Some(verified_id) = credentials.get("github_user_id") else {
44 return Ok(AuthStep::Failed);
45 };
46
47 if stored_id.as_bytes().ct_eq(verified_id.as_bytes()).into() {
48 Ok(AuthStep::Authenticated)
49 } else {
50 Ok(AuthStep::Failed)
51 }
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn test_create_stores_user_id_and_login() {
61 let config = HashMap::from([
62 ("user_id".to_string(), "583231".to_string()),
63 ("login".to_string(), "octocat".to_string()),
64 ]);
65
66 let stored = GithubProvider.create(&Rng::default(), &config).unwrap();
67 assert_eq!(stored.get("user_id").unwrap(), "583231");
68 assert_eq!(stored.get("login").unwrap(), "octocat");
69 }
70
71 #[test]
72 fn test_create_requires_user_id() {
73 assert!(GithubProvider.create(&Rng::default(), &HashMap::new()).is_err());
74 }
75
76 #[test]
77 fn test_create_rejects_non_numeric_user_id() {
78 let config = HashMap::from([("user_id".to_string(), "octocat".to_string())]);
81 assert!(GithubProvider.create(&Rng::default(), &config).is_err());
82 }
83
84 #[test]
85 fn test_create_rejects_empty_user_id() {
86 let config = HashMap::from([("user_id".to_string(), "".to_string())]);
87 assert!(GithubProvider.create(&Rng::default(), &config).is_err());
88 }
89
90 #[test]
91 fn test_authenticate_matching_user_id() {
92 let stored = HashMap::from([("user_id".to_string(), "583231".to_string())]);
93 let credentials = HashMap::from([("github_user_id".to_string(), "583231".to_string())]);
94
95 let step = GithubProvider.authenticate(&stored, &credentials).unwrap();
96 assert_eq!(step, AuthStep::Authenticated);
97 }
98
99 #[test]
100 fn test_authenticate_mismatched_user_id_fails() {
101 let stored = HashMap::from([("user_id".to_string(), "583231".to_string())]);
102 let credentials = HashMap::from([("github_user_id".to_string(), "999999".to_string())]);
103
104 let step = GithubProvider.authenticate(&stored, &credentials).unwrap();
105 assert_eq!(step, AuthStep::Failed);
106 }
107
108 #[test]
109 fn test_authenticate_without_verified_user_id_fails() {
110 let stored = HashMap::from([("user_id".to_string(), "583231".to_string())]);
113 let credentials = HashMap::from([
114 ("code".to_string(), "some-oauth-code".to_string()),
115 ("state".to_string(), "some-state".to_string()),
116 ]);
117
118 let step = GithubProvider.authenticate(&stored, &credentials).unwrap();
119 assert_eq!(step, AuthStep::Failed);
120 }
121}