armature_auth/providers/
github.rs

1//! GitHub OAuth2 Provider
2
3use crate::error::AuthError;
4use crate::oauth2::OAuth2Config;
5use serde::{Deserialize, Serialize};
6
7const AUTH_URL: &str = "https://github.com/login/oauth/authorize";
8const TOKEN_URL: &str = "https://github.com/login/oauth/access_token";
9const USER_INFO_URL: &str = "https://api.github.com/user";
10const USER_EMAIL_URL: &str = "https://api.github.com/user/emails";
11
12/// GitHub user information
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GitHubUser {
15    pub id: u64,
16    pub login: String,
17    pub name: Option<String>,
18    pub email: Option<String>,
19    pub avatar_url: Option<String>,
20    pub bio: Option<String>,
21    pub company: Option<String>,
22    pub location: Option<String>,
23}
24
25/// GitHub email information
26#[derive(Debug, Clone, Serialize, Deserialize)]
27struct GitHubEmail {
28    email: String,
29    primary: bool,
30    verified: bool,
31}
32
33/// GitHub OAuth2 provider
34pub struct GitHubProvider;
35
36impl GitHubProvider {
37    /// Create a new GitHub OAuth2 configuration
38    pub fn config(client_id: String, client_secret: String, redirect_url: String) -> OAuth2Config {
39        OAuth2Config::new(
40            client_id,
41            client_secret,
42            AUTH_URL.to_string(),
43            TOKEN_URL.to_string(),
44            redirect_url,
45        )
46        .with_scopes(vec!["user:email".to_string()])
47        .with_user_info_url(USER_INFO_URL.to_string())
48    }
49
50    /// Fetch user info from GitHub
51    pub async fn get_user_info(access_token: &str) -> Result<GitHubUser, AuthError> {
52        let client = reqwest::Client::new();
53
54        // Get user profile
55        let mut user: GitHubUser = client
56            .get(USER_INFO_URL)
57            .header("Authorization", format!("token {}", access_token))
58            .header("User-Agent", "Armature-Auth")
59            .send()
60            .await
61            .map_err(|e| AuthError::HttpRequest(e.to_string()))?
62            .json()
63            .await
64            .map_err(|e| AuthError::InvalidResponse(e.to_string()))?;
65
66        // If email is not in profile, fetch from emails endpoint
67        if user.email.is_none() {
68            let emails: Vec<GitHubEmail> = client
69                .get(USER_EMAIL_URL)
70                .header("Authorization", format!("token {}", access_token))
71                .header("User-Agent", "Armature-Auth")
72                .send()
73                .await
74                .map_err(|e| AuthError::HttpRequest(e.to_string()))?
75                .json()
76                .await
77                .map_err(|e| AuthError::InvalidResponse(e.to_string()))?;
78
79            // Find primary verified email
80            user.email = emails
81                .iter()
82                .find(|e| e.primary && e.verified)
83                .or_else(|| emails.first())
84                .map(|e| e.email.clone());
85        }
86
87        Ok(user)
88    }
89}