Skip to main content

reifydb_auth/method/
github.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use 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		// The user_id is the immutable github account id; a login name here would
79		// let an account be hijacked after a github username change or reuse.
80		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		// github_user_id is injected by the auth service only after it verified the
111		// oauth token with github; raw client credentials must never authenticate.
112		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}