Skip to content

Commit ce7e5c7

Browse files
committed
init
0 parents  commit ce7e5c7

11 files changed

+699
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
target
2+
Cargo.lock

Cargo.toml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "gcp_auth"
3+
version = "0.1.0"
4+
authors = ["Peter Hrvola <[email protected]>"]
5+
repository = "https://github.com/hrvolapeter/gcp_auth"
6+
description = "Google cloud platform (GCP) authentication using default and custom service accounts"
7+
documentation = "https://docs.rs/gcp_auth/"
8+
keywords = ["authentication", "gcp", "google"]
9+
categories = ["asynchronous", "authentication"]
10+
readme = "README.md"
11+
license = "MIT"
12+
edition = "2018"
13+
14+
[dependencies]
15+
base64 = "0.12"
16+
chrono = { version = "0.4", features = ["serde"] }
17+
hyper = "0.13.5"
18+
hyper-tls = "0.4"
19+
log = "0.4"
20+
rustls = "0.17"
21+
serde = {version = "1.0", features = ["derive"]}
22+
serde_json = "1.0"
23+
tokio = { version = "0.2", features = ["fs"] }
24+
url = "2"
25+
bytes = "0.5"
26+
async-trait = "0.1.31"
27+
thiserror = "1.0"

LICENSE

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
Copyright (c) 2020 Peter Hrvola
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.
20+
21+
[yup-oauth2]
22+
23+
Copyright (c) 2015 The yup-oauth2 Developers
24+
25+
Permission is hereby granted, free of charge, to any person obtaining a copy
26+
of this software and associated documentation files (the "Software"), to deal
27+
in the Software without restriction, including without limitation the rights
28+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
29+
copies of the Software, and to permit persons to whom the Software is
30+
furnished to do so, subject to the following conditions:
31+
32+
The above copyright notice and this permission notice shall be included in all
33+
copies or substantial portions of the Software.
34+
35+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
38+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
40+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
41+
SOFTWARE.
42+
43+
[JWT]
44+
45+
Copyright (c) 2016 Google Inc. ([email protected]) -- though not an official
46+
Google product or in any way related!
47+
48+
Permission is hereby granted, free of charge, to any person obtaining a copy
49+
of this software and associated documentation files (the "Software"), to
50+
deal in the Software without restriction, including without limitation the
51+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
52+
sell copies of the Software, and to permit persons to whom the Software is
53+
furnished to do so, subject to the following conditions:
54+
55+
The above copyright notice and this permission notice shall be included in
56+
all copies or substantial portions of the Software.
57+
58+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
59+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
60+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
61+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
62+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
63+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS

README.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# GCP Auth
2+
[![crates.io](https://img.shields.io/crates/v/gcp_auth.svg)](https://crates.io/crates/gcp_auth)
3+
4+
GCP Auth is a simple, minimal authentication library for Google Cloud Platform (GCP) providing authentication using
5+
services accounts that are used to issues Bearer tokens that can be used to authenticate against GCP services.
6+
7+
Library implements two authenticatiom methods:
8+
9+
- Default service accounts - can be used inside GCP
10+
- Custom service account - provided using environenment variable
11+
12+
Tokens should not be cached in the application and before every use a new token should be request. The GCP auth library decides
13+
if there is available token with appropriate scope or if a new token should be generated.
14+
15+
## Default Service Account
16+
17+
When running inside GCP the library can be asked directly without any further configuration to provide a Bearer token
18+
for the current service account of the service.
19+
20+
```rust
21+
let authentication_manager = gcp_auth::init();
22+
let token = authentication_manager.get_token().await?;
23+
```
24+
25+
## Custom Service Account
26+
27+
When running outside of GCP e.g on development laptop to allow finer granularity for permission a
28+
custom service account can be used. To use a custom service account a configuration file containing key
29+
has to be downloaded in IAM service for the service account you intend to use. The configuration file has to
30+
be available to the application at run time. The path to the configuration file is specified by
31+
`GOOGLE_APPLICATION_CREDENTIALS` environment variable.
32+
33+
```no_run
34+
# GOOGLE_APPLICATION_CREDENTIALS environtment variable is set-up
35+
let authentication_manager = gcp_auth::init();
36+
let token = authentication_manager.get_token().await?;
37+
```
38+
39+
# License
40+
Parts of implementatino have been sourced from [yup-oauth2](https://github.com/dermesser/yup-oauth2)
41+
42+
Licensed under [MIT license](http://opensource.org/licenses/MIT).

src/authentication_manager.rs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use crate::prelude::*;
2+
use tokio::sync::Mutex;
3+
4+
#[async_trait]
5+
pub trait ServiceAccount: Send {
6+
fn get_token(&self, scopes: &[&str]) -> Option<Token>;
7+
async fn refresh_token(&mut self, client: &HyperClient, scopes: &[&str]) -> Result<(), GCPAuthError>;
8+
}
9+
10+
/// Authentication manager is responsible for caching and obtaing credentials for the required scope
11+
///
12+
/// Cacheing for the full life time is ensured
13+
pub struct AuthenticationManager {
14+
pub(crate) client: HyperClient,
15+
pub(crate) service_account: Mutex<Box<dyn ServiceAccount>>,
16+
}
17+
18+
impl AuthenticationManager {
19+
/// Requests Bearer token for the provided scope
20+
///
21+
/// Token can be used in the request authorization header in format "Bearer {token}"
22+
pub async fn get_token(&self, scopes: &[&str]) -> BoxResult<Token> {
23+
let mut sa = self.service_account.lock().await;
24+
let mut token = sa.get_token(scopes);
25+
if token.is_none() {
26+
sa.refresh_token(&self.client, scopes).await?;
27+
token = sa.get_token(scopes);
28+
}
29+
30+
Ok(token.expect("Token obtained with refresh or failed before"))
31+
}
32+
}

src/custom_service_account.rs

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use crate::prelude::*;
2+
use crate::authentication_manager::ServiceAccount;
3+
use tokio::fs;
4+
5+
#[derive(Debug)]
6+
pub struct CustomServiceAccount {
7+
tokens: HashMap<Vec<String>, Token>,
8+
credentials: ApplicationCredentials,
9+
}
10+
11+
impl CustomServiceAccount {
12+
const GOOGLE_APPLICATION_CREDENTIALS: &'static str = "GOOGLE_APPLICATION_CREDENTIALS";
13+
14+
pub async fn new() -> Result<Self, GCPAuthError> {
15+
let path = std::env::var(Self::GOOGLE_APPLICATION_CREDENTIALS).map_err(|_| GCPAuthError::AplicationProfileMissing)?;
16+
let credentials = ApplicationCredentials::from_file(path).await?;
17+
Ok(Self {
18+
credentials,
19+
tokens: HashMap::new(),
20+
})
21+
}
22+
}
23+
24+
#[async_trait]
25+
impl ServiceAccount for CustomServiceAccount {
26+
fn get_token(&self, scopes: &[&str]) -> Option<Token> {
27+
let key: Vec<_> = scopes.iter().map(|x| (*x).to_string()).collect();
28+
let token = self
29+
.tokens
30+
.get(&key);
31+
32+
if token.is_none() || token.unwrap().has_expired() {
33+
return None;
34+
}
35+
Some(token.unwrap().clone())
36+
}
37+
38+
async fn refresh_token(&mut self, client: &HyperClient, scopes: &[&str]) -> Result<(), GCPAuthError> {
39+
use crate::jwt::Claims;
40+
use crate::jwt::JWTSigner;
41+
use crate::jwt::GRANT_TYPE;
42+
use hyper::header;
43+
use url::form_urlencoded;
44+
45+
let signer = JWTSigner::new(&self.credentials.private_key)?;
46+
47+
let claims = Claims::new(&self.credentials, scopes, None);
48+
let signed = signer.sign_claims(&claims).map_err(GCPAuthError::TLSError)?;
49+
let rqbody = form_urlencoded::Serializer::new(String::new())
50+
.extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", signed.as_str())])
51+
.finish();
52+
let request = hyper::Request::post(&self.credentials.token_uri)
53+
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
54+
.body(hyper::Body::from(rqbody))
55+
.unwrap();
56+
log::debug!("requesting token from service account: {:?}", request);
57+
let (head, body) = client.request(request).await.map_err(GCPAuthError::OAuthConnectionError)?.into_parts();
58+
let body = hyper::body::to_bytes(body).await.map_err(GCPAuthError::OAuthConnectionError)?;
59+
log::debug!("received response; head: {:?}, body: {:?}", head, body);
60+
let token: Token = serde_json::from_slice(&body).map_err(GCPAuthError::OAuthParsingError)?;
61+
let key = scopes.iter().map(|x| (*x).to_string()).collect();
62+
self.tokens.insert(key, token);
63+
Ok(())
64+
}
65+
}
66+
67+
#[derive(Serialize, Deserialize, Debug, Clone)]
68+
pub struct ApplicationCredentials {
69+
pub r#type: Option<String>,
70+
/// project_id
71+
pub project_id: Option<String>,
72+
/// private_key_id
73+
pub private_key_id: Option<String>,
74+
/// private_key
75+
pub private_key: String,
76+
/// client_email
77+
pub client_email: String,
78+
/// client_id
79+
pub client_id: Option<String>,
80+
/// auth_uri
81+
pub auth_uri: Option<String>,
82+
/// token_uri
83+
pub token_uri: String,
84+
/// auth_provider_x509_cert_url
85+
pub auth_provider_x509_cert_url: Option<String>,
86+
/// client_x509_cert_url
87+
pub client_x509_cert_url: Option<String>,
88+
}
89+
90+
impl ApplicationCredentials {
91+
async fn from_file<T: AsRef<Path>>(path: T) -> Result<ApplicationCredentials, GCPAuthError> {
92+
let content = fs::read_to_string(path).await.map_err(GCPAuthError::AplicationProfilePath)?;
93+
Ok(serde_json::from_str(&content).map_err(GCPAuthError::AplicationProfileFormat)?)
94+
}
95+
}

src/default_service_account.rs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use crate::prelude::*;
2+
use crate::authentication_manager::ServiceAccount;
3+
use hyper::body::Body;
4+
use hyper::Method;
5+
6+
#[derive(Debug)]
7+
pub struct DefaultServiceAccount {
8+
token: Token,
9+
}
10+
11+
impl DefaultServiceAccount {
12+
const DEFAULT_TOKEN_GCP_URI: &'static str = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
13+
14+
pub async fn new(client: &HyperClient) -> Result<Self, GCPAuthError> {
15+
let token = Self::get_token(client).await?;
16+
Ok(Self { token })
17+
}
18+
19+
fn build_token_request() -> Request<Body> {
20+
Request::builder()
21+
.method(Method::GET)
22+
.uri(Self::DEFAULT_TOKEN_GCP_URI)
23+
.header("Metadata-Flavor", "Google")
24+
.body(Body::empty()).unwrap()
25+
}
26+
27+
async fn get_token(client: &HyperClient) -> Result<Token, GCPAuthError> {
28+
log::debug!("Getting token from GCP instance metadata server");
29+
let req = Self::build_token_request();
30+
let resp = client.request(req).await.map_err(GCPAuthError::MetadataConnectionError)?;
31+
if resp.status().is_success() {
32+
let body = hyper::body::aggregate(resp).await.map_err(GCPAuthError::MetadataConnectionError)?;
33+
let bytes = body.bytes();
34+
let token = serde_json::from_slice(bytes).map_err(GCPAuthError::MetadataParsingError)?;
35+
return Ok(token);
36+
}
37+
Err(GCPAuthError::MetadataServerUnavailable)
38+
}
39+
}
40+
41+
#[async_trait]
42+
impl ServiceAccount for DefaultServiceAccount {
43+
fn get_token(&self, _scopes: &[&str]) -> Option<Token> {
44+
if self.token.has_expired() {
45+
return None;
46+
}
47+
Some(self.token.clone())
48+
}
49+
50+
async fn refresh_token(&mut self, client: &HyperClient, _scopes: &[&str]) -> Result<(), GCPAuthError> {
51+
let token = Self::get_token(client).await?;
52+
self.token = token;
53+
Ok(())
54+
}
55+
}

0 commit comments

Comments
 (0)