Skip to content

Commit

Permalink
Fix examples, update rocket integration
Browse files Browse the repository at this point in the history
  • Loading branch information
David Graeff committed Jan 22, 2024
1 parent c169f1f commit ce5a9f5
Show file tree
Hide file tree
Showing 16 changed files with 128 additions and 101 deletions.
13 changes: 5 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.8.0] - 2024-01-22

### Added

- Credentials::download_google_jwks(): Update/replace public keys. Useful for long running services.
Expand All @@ -14,14 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Support for reqwest 0.11 / Tokio 1.0
- Prefer to use `AsRef<str>` when passing params
- Add ability to read raw document contents (without deserializing the JSON)
- [Breaking] Change Credentials::new: No JWKSet parameter, use with_jwkset or download_jwkset
- [Breaking] Change Credentials::from_file: Do not download jwks anymore. Use with_jwkset or download_jwkset.
- [Breaking] Rename JWKSetDTO to JWKSet
- [Breaking] jwt::download_google_jwks returns a string and not a DTO anymore for better error reporting
- [Breaking] jwt::download_google_jwks_async is behind the unstable feature now, as originally intended
- [Breaking] Async only API
- [Breaking] Rocket example uses the Rocket 0.5 release
- [Breaking] Migrated to Rust edition 2021

## [0.6.1] - 2020-11-12

Expand Down
17 changes: 10 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "firestore-db-and-auth"
version = "0.8.0"
authors = ["David Gräff <david.graeff@web.de>"]
edition = "2018"
edition = "2021"
license = "MIT"
description = "This crate allows easy access to your Google Firestore DB via service account or OAuth impersonated Google Firebase Auth credentials."
readme = "readme.md"
Expand All @@ -14,21 +14,24 @@ repository = "https://github.com/davidgraeff/firestore-db-and-auth-rs"
[dependencies]
bytes = "1.1"
cache_control = "0.2"
reqwest = { version = "0.11", default-features = false, features = ["json", "blocking"] }
reqwest = { version = "0.11", default-features = false, features = ["json", "blocking", "hyper-rustls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
biscuit = "0.5"
ring = "0.16"
base64 = "0.13"
biscuit = "0.7"
ring = "0.17"
base64 = "0.21"
async-trait = "0.1"
tokio = { version = "1.13", features = ["macros"] }
futures = "0.3"
pin-project = "1.0"
http = "0.2"
http = "1.0"

[dev-dependencies]
tokio-test = "0.4"

[dependencies.rocket]
version = "0.4.6"
version = "0.5.0"
default-features = false
optional = true

Expand Down
8 changes: 4 additions & 4 deletions examples/create_read_write_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ async fn main() -> errors::Result<()> {
credential_file.with_file_name("cached_jwks.jwks").as_path(),
&cred
).await?;
cred.add_jwks_public_keys(&jwkset);
cred.add_jwks_public_keys(&jwkset).await;
cred.verify().await?;

// Perform some db operations via a service account session
Expand All @@ -235,12 +235,12 @@ async fn valid_test_credentials() -> errors::Result<Credentials> {
let mut jwks_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
jwks_path.push("firebase-service-account.jwks");

let mut cred: Credentials = Credentials::new(include_str!("../tests/service-account-test.json"))?;
let cred: Credentials = Credentials::new(include_str!("../firebase-service-account.json")).await?;

// Only download the public keys once, and cache them.
let jwkset = utils::from_cache_file(jwks_path.as_path(), &cred).await?;
cred.add_jwks_public_keys(&jwkset);
cred.verify()?;
cred.add_jwks_public_keys(&jwkset).await;
cred.verify().await?;

Ok(cred)
}
Expand Down
11 changes: 8 additions & 3 deletions examples/own_auth.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use firestore_db_and_auth::{documents, errors, Credentials, FirebaseAuthBearer};
use firestore_db_and_auth::errors::FirebaseError::APIError;

/// Define your own structure that will implement the FirebaseAuthBearer trait
struct MyOwnSession {
Expand Down Expand Up @@ -29,8 +30,7 @@ impl FirebaseAuthBearer for MyOwnSession {
}
}

#[tokio::main]
async fn main() -> errors::Result<()> {
async fn run() -> errors::Result<()> {
let credentials = Credentials::from_file("firebase-service-account.json").await?;
#[derive(serde::Serialize)]
struct TestData {
Expand All @@ -55,9 +55,14 @@ async fn main() -> errors::Result<()> {
Ok(())
}

#[tokio::main]
async fn main() -> errors::Result<()> {
run().await
}

#[tokio::test]
async fn own_auth_test() {
if let Err(APIError(code, str_code, context)) = main() {
if let Err(APIError(code, str_code, context)) = run().await {
assert_eq!(str_code, "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.");
assert_eq!(context, "test_doc");
assert_eq!(code, 401);
Expand Down
2 changes: 1 addition & 1 deletion examples/session_cookie.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async fn main() -> Result<(), FirebaseError> {
let jwkset = utils::from_cache_file(
credential_file.with_file_name("cached_jwks.jwks").as_path(), &cred
).await?;
cred.add_jwks_public_keys(&jwkset);
cred.add_jwks_public_keys(&jwkset).await;
cred.verify().await?;

let user_session = utils::user_session_with_cached_refresh_token(&cred).await?;
Expand Down
14 changes: 8 additions & 6 deletions examples/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ pub async fn from_cache_file(cache_file: &std::path::Path, c: &Credentials) -> e
} else {
// If not present, download the two jwks (specific service account + google system account),
// merge them into one set of keys and store them in the cache file.
let mut jwks = JWKSet::new(&download_google_jwks(&c.client_email).await?.0)?;
jwks.keys
.append(&mut JWKSet::new(&download_google_jwks("securetoken@system.gserviceaccount.com").await?.0)?.keys);
let jwk_set_1 = download_google_jwks(&c.client_email).await?;
let jwk_set_2 = download_google_jwks("securetoken@system.gserviceaccount.com").await?;

let mut jwks = JWKSet::new(&jwk_set_1.0)?;
jwks.keys.append(&mut JWKSet::new(&jwk_set_2.0)?.keys);
let f = File::create(cache_file)?;
serde_json::to_writer_pretty(f, &jwks)?;
jwks
Expand All @@ -68,12 +70,12 @@ pub async fn valid_test_credentials() -> errors::Result<Credentials> {
let mut jwks_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
jwks_path.push("firebase-service-account.jwks");

let mut cred: Credentials = Credentials::new(include_str!("../../tests/service-account-test.json"))?;
let cred: Credentials = Credentials::new(include_str!("../../firebase-service-account.json")).await?;

// Only download the public keys once, and cache them.
let jwkset = from_cache_file(jwks_path.as_path(), &cred).await?;
cred.add_jwks_public_keys(&jwkset);
cred.verify()?;
cred.add_jwks_public_keys(&jwkset).await;
cred.verify().await?;

Ok(cred)
}
28 changes: 15 additions & 13 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Limitations:

This crate operates on DTOs (Data transfer objects) for type-safe operations on your Firestore DB.

```rust
```rust,no_run
use firestore_db_and_auth::{Credentials, ServiceSession, documents, errors::Result};
use serde::{Serialize,Deserialize};
Expand Down Expand Up @@ -78,20 +78,22 @@ fn write_partial(session: &ServiceSession) -> Result<()> {

Read the document with the id "service_test" from the Firestore "tests" collection:

```rust
```rust,no_run
use firestore_db_and_auth::{documents};
let obj : DemoDTO = documents::read(&session, "tests", "service_test")?;
```

For listing all documents of the "tests" collection you want to use the `List` struct which implements the `Iterator` trait.
It will hide the complexity of the paging API and fetches new documents when necessary:

```rust
```rust,no_run
use firestore_db_and_auth::{documents};
let values: documents::List<DemoDTO, _> = documents::list(&session, "tests");
for doc_result in values {
let mut stream = documents::list(&session, "tests");
while let Some(Ok(doc_result)) = stream.next().await {
// The document is wrapped in a Result<> because fetching new data could have failed
let (doc, _metadata) = doc_result?;
let (doc, _metadata) = doc_result;
let doc: DemoDTO = doc;
println!("{:?}", doc);
}
```
Expand All @@ -102,7 +104,7 @@ You cannot keep the iterator for long or expect new documents to appear in an on
For quering the database you would use the `query` method.
In the following example the collection "tests" is queried for document(s) with the "id" field equal to "Sam Weiss".

```rust
```rust,no_run
use firestore_db_and_auth::{documents, dto};
let values = documents::query(&session, "tests", "Sam Weiss".into(), dto::FieldOperator::EQUAL, "id")?;
Expand All @@ -128,7 +130,7 @@ This custom error type wraps all possible errors (IO, Reqwest, JWT errors etc)
and Google REST API errors. If you want to specifically check for an API error,
you could do so:

```rust
```rust,no_run
use firestore_db_and_auth::{documents, errors::FirebaseError};
let r = documents::delete(&session, "tests/non_existing", true);
Expand All @@ -151,7 +153,7 @@ It may be the collection or document id or any other context information.
The file should contain `"private_key_id": ...`.
2. Add another field `"api_key" : "YOUR_API_KEY"` and replace YOUR_API_KEY with your *Web API key*, to be found in the [Google Firebase console](https://console.firebase.google.com) in "Project Overview -> Settings - > General".

```rust
```rust,no_run
use firestore_db_and_auth::{Credentials, ServiceSession};
/// Create credentials object. You may as well do that programmatically.
Expand All @@ -170,7 +172,7 @@ let session = ServiceSession::new(&cred)
You can create a user session in various ways.
If you just have the firebase Auth user_id, you would follow these steps:

```rust
```rust,no_run
use firestore_db_and_auth::{Credentials, sessions};
/// Create credentials object. You may as well do that programmatically.
Expand All @@ -187,14 +189,14 @@ let session = UserSession::by_user_id(&cred, "the_user_id")

If you already have a valid refresh token and want to generate an access token (and a session object), you do this instead:

```rust
```rust,no_run
let refresh_token = "fkjandsfbajsbfd;asbfdaosa.asduabsifdabsda,fd,a,sdbasfadfasfas.dasdasbfadusbflansf";
let session = UserSession::by_refresh_token(&cred, &refresh_token)?;
```

Another way of retrieving a session object is by providing a valid access token like so:

```rust
```rust,no_run
let access_token = "fkjandsfbajsbfd;asbfdaosa.asduabsifdabsda,fd,a,sdbasfadfasfas.dasdasbfadusbflansf";
let session = UserSession::by_access_token(&cred, &access_token)?;
```
Expand All @@ -220,7 +222,7 @@ First download the 2 public key files:

Create a `Credentials` object like so:

```rust
```rust,no_run
use firestore_db_and_auth::Credentials;
let c = Credentials::new(include_str!("firebase-service-account.json"))?
.with_jwkset(&JWKSet::new(include_str!("firebase-service-account.jwks"))?)?;
Expand Down
45 changes: 25 additions & 20 deletions src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::BTreeMap;
use std::fs::File;
use std::sync::Arc;
use std::sync::{Arc};
use std::fmt;
use tokio::sync::RwLock;

use super::jwt::{create_jwt_encoded, download_google_jwks, verify_access_token, JWKSet, JWT_AUDIENCE_IDENTITY};
use crate::{errors::FirebaseError, jwt::TokenValidationResult};
use std::io::BufReader;
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use tokio::sync::RwLock;

type Error = super::errors::FirebaseError;

Expand Down Expand Up @@ -54,14 +56,15 @@ pub struct Credentials {
pub client_email: String,
pub client_id: String,
pub api_key: String,
/// The public keys. Those will rotate over time.
/// Altering the keys is still a rare operation, so access should
/// be optimized for reading, hence the RwLock.
#[serde(default, skip)]
pub(crate) keys: Arc<RwLock<Keys>>,
}

/// Converts a PEM (ascii base64) encoded private key into the binary der representation
pub fn pem_to_der(pem_file_contents: &str) -> Result<Vec<u8>, Error> {
use base64::decode;

let pem_file_contents = pem_file_contents
.find("-----BEGIN")
// Cut off the first BEGIN part
Expand All @@ -77,7 +80,7 @@ pub fn pem_to_der(pem_file_contents: &str) -> Result<Vec<u8>, Error> {
}

let base64_body = pem_file_contents.unwrap().replace("\n", "");
Ok(decode(&base64_body)
Ok(BASE64_STANDARD.decode(&base64_body)
.map_err(|_| FirebaseError::Generic("Invalid private key in credentials file. Expected Base64 data."))?)
}

Expand Down Expand Up @@ -113,9 +116,10 @@ impl Credentials {
/// use firestore_db_and_auth::{Credentials};
/// use firestore_db_and_auth::jwt::JWKSet;
///
/// let c: Credentials = Credentials::new(include_str!("../tests/service-account-test.json"))?
/// .with_jwkset(&JWKSet::new(include_str!("../tests/service-account-test.jwks"))?)?;
/// # Ok::<(), firestore_db_and_auth::errors::FirebaseError>(())
/// # tokio_test::block_on(async {
/// let c: Credentials = Credentials::new(include_str!("../tests/service-account-test.json")).await.unwrap()
/// .with_jwkset(&JWKSet::new(include_str!("../tests/service-account-test.jwks")).unwrap()).await.unwrap();
/// # })
/// ```
///
/// You need two JWKS files for this crate to work:
Expand Down Expand Up @@ -162,9 +166,10 @@ impl Credentials {
/// ```no_run
/// use firestore_db_and_auth::{Credentials};
///
/// let c: Credentials = Credentials::new(include_str!("../tests/service-account-test.json"))?
/// .download_jwkset()?;
/// # Ok::<(), firestore_db_and_auth::errors::FirebaseError>(())
/// # tokio_test::block_on(async {
/// let c: Credentials = Credentials::new(include_str!("../tests/service-account-test.json")).await.unwrap()
/// .download_jwkset().await.unwrap();
/// # })
/// ```
pub async fn download_jwkset(self) -> Result<Credentials, Error> {
self.download_google_jwks().await?;
Expand Down Expand Up @@ -217,24 +222,24 @@ impl Credentials {
/// use firestore_db_and_auth::credentials::Credentials;
/// use firestore_db_and_auth::JWKSet;
///
/// let mut c : Credentials = serde_json::from_str(include_str!("../tests/service-account-test.json"))?;
/// c.add_jwks_public_keys(&JWKSet::new(include_str!("../tests/service-account-test.jwks"))?);
/// c.compute_secret()?;
/// c.verify()?;
/// # Ok::<(), firestore_db_and_auth::errors::FirebaseError>(())
/// # tokio_test::block_on(async {
/// let mut c : Credentials = serde_json::from_str(include_str!("../tests/service-account-test.json")).unwrap();
/// c.add_jwks_public_keys(&JWKSet::new(include_str!("../tests/service-account-test.jwks")).unwrap()).await;
/// c.compute_secret().await.unwrap();
/// c.verify().await.unwrap();
/// # })
/// ```
pub async fn add_jwks_public_keys(&self, jwkset: &JWKSet) {
let mut keys = self.keys.write().await;
let key_lock = self.keys.write();
let keys = &mut key_lock.await.pub_key;

for entry in jwkset.keys.iter() {
if !entry.headers.key_id.is_some() {
continue;
}

let key_id = entry.headers.key_id.as_ref().unwrap().to_owned();
keys
.pub_key
.insert(key_id, Arc::new(entry.ne.jws_public_key_secret()));
keys.insert(key_id, Arc::new(entry.ne.jws_public_key_secret()));
}
}

Expand Down
Loading

0 comments on commit ce5a9f5

Please sign in to comment.