1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use std::path::{Path, PathBuf};

use chrono::Utc;
use futures_util::TryFutureExt;
use serde::{Deserialize, Serialize};
use synd_auth::jwt::google::JwtError;
use thiserror::Error;
use tracing::debug;

use crate::{application::JwtService, config};

#[derive(Debug, Clone, Copy)]
pub enum AuthenticationProvider {
    Github,
    Google,
}

#[derive(Debug, Error)]
pub enum CredentialError {
    #[error("google jwt expired")]
    GoogleJwtExpired { refresh_token: String },
    #[error("google jwt email not verified")]
    GoogleJwtEmailNotVerified,
    #[error("failed to open: {0}")]
    Open(std::io::Error),
    #[error("deserialize credential: {0}")]
    Deserialize(serde_json::Error),
    #[error("decode jwt: {0}")]
    DecodeJwt(JwtError),
    #[error("refresh jwt id token: {0}")]
    RefreshJwt(JwtError),
    #[error("persist credential: {0}")]
    PersistCredential(anyhow::Error),
}

#[derive(Serialize, Deserialize, Clone)]
pub enum Credential {
    Github {
        access_token: String,
    },
    Google {
        id_token: String,
        refresh_token: String,
    },
}

impl Credential {
    async fn restore_from_path(
        path: &Path,
        jwt_service: &JwtService,
    ) -> Result<Self, CredentialError> {
        tracing::info!(
            path = path.display().to_string(),
            "Restore credential from cache"
        );
        let mut f = std::fs::File::open(path).map_err(CredentialError::Open)?;
        let credential = serde_json::from_reader(&mut f).map_err(CredentialError::Deserialize)?;

        match &credential {
            Credential::Github { .. } => Ok(credential),
            Credential::Google {
                id_token,
                refresh_token,
            } => {
                let claims = jwt_service
                    .google
                    .decode_id_token_insecure(id_token, false)
                    .map_err(CredentialError::DecodeJwt)?;
                if !claims.email_verified {
                    return Err(CredentialError::GoogleJwtEmailNotVerified);
                }
                tracing::info!("{claims:?}");
                if !claims.is_expired(Utc::now()) {
                    return Ok(credential);
                }

                tracing::info!("Google jwt expired, trying to refresh");

                let id_token = jwt_service
                    .google
                    .refresh_id_token(refresh_token)
                    .await
                    .map_err(CredentialError::RefreshJwt)?;

                let credential = Credential::Google {
                    id_token,
                    refresh_token: refresh_token.clone(),
                };

                persist_credential(&credential).map_err(CredentialError::PersistCredential)?;

                tracing::info!("Persist refreshed credential");

                Ok(credential)
            }
        }
    }
}

pub fn persist_credential(cred: &Credential) -> anyhow::Result<()> {
    let cred_path = cred_file();
    if let Some(parent) = cred_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let mut cred_file = std::fs::File::create(&cred_path)?;

    debug!(path = ?cred_path.display(), "Create credential cache file");

    serde_json::to_writer(&mut cred_file, &cred)?;

    Ok(())
}

fn cred_file() -> PathBuf {
    config::cache_dir().join("credential.json")
}

pub async fn credential_from_cache(jwt_decoders: &JwtService) -> Option<Credential> {
    Credential::restore_from_path(cred_file().as_path(), jwt_decoders)
        .inspect_err(|err| {
            tracing::error!("Restore credential from cache: {err}");
        })
        .await
        .ok()
}