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
use async_trait::async_trait;
use jsonwebtoken::Validation;
use serde::Deserialize;
use time::OffsetDateTime;
use urlencoding::encode;

use google_cloud_metadata::{METADATA_FLAVOR_KEY, METADATA_GOOGLE, METADATA_HOST_ENV, METADATA_IP};

use crate::error::Error;
use crate::token::Token;
use crate::token_source::{default_http_client, TokenSource};

/// Fetches a JWT token from the metadata server.
/// using the `identity` endpoint.
///
/// This token source is useful for service-to-service authentication, notably on Cloud Run.
///
/// See <https://cloud.google.com/run/docs/authenticating/service-to-service#use_the_metadata_server>
#[derive(Clone)]
pub struct ComputeIdentitySource {
    token_url: String,
    client: reqwest::Client,
    decoding_key: jsonwebtoken::DecodingKey,
    validation: jsonwebtoken::Validation,
}

impl std::fmt::Debug for ComputeIdentitySource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ComputeIdentitySource")
            .field("token_url", &self.token_url)
            .finish_non_exhaustive()
    }
}

impl ComputeIdentitySource {
    pub(crate) fn new(audience: &str) -> Result<ComputeIdentitySource, Error> {
        let host = match std::env::var(METADATA_HOST_ENV) {
            Ok(s) => s,
            Err(_e) => METADATA_IP.to_string(),
        };

        // Only used to extract the expiry without checking the signature.
        let mut validation = Validation::default();
        validation.insecure_disable_signature_validation();
        let decoding_key = jsonwebtoken::DecodingKey::from_secret(b"");

        Ok(ComputeIdentitySource {
            token_url: format!(
                "http://{}/computeMetadata/v1/instance/service-accounts/default/identity?audience={}",
                host,
                encode(audience)
            ),
            client: default_http_client(),
            decoding_key,
            validation,
        })
    }
}

#[derive(Deserialize)]
struct ExpClaim {
    exp: i64,
}

#[async_trait]
impl TokenSource for ComputeIdentitySource {
    async fn token(&self) -> Result<Token, Error> {
        let jwt = self
            .client
            .get(self.token_url.to_string())
            .header(METADATA_FLAVOR_KEY, METADATA_GOOGLE)
            .send()
            .await?
            .text()
            .await?;

        let exp = jsonwebtoken::decode::<ExpClaim>(&jwt, &self.decoding_key, &self.validation)?
            .claims
            .exp;

        Ok(Token {
            access_token: jwt,
            token_type: "Bearer".into(),
            expiry: OffsetDateTime::from_unix_timestamp(exp).ok(),
        })
    }
}