Skip to content

Commit

Permalink
Merge pull request #18 from trchopan/apply-generic-token-type
Browse files Browse the repository at this point in the history
Verifier take generic typing to be extended in case of Custom Claims
  • Loading branch information
trchopan authored Feb 2, 2024
2 parents 702976b + 264d8ac commit a3a3f5b
Show file tree
Hide file tree
Showing 12 changed files with 283 additions and 31 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ jobs:
run: cargo build -p example-axum-basic
- name: Build example axum sqlite
run: cargo build -p example-axum-sqlite
- name: Build example actix custom claims
run: cargo build -p example-actix-custom-claims
- name: Build example axum custom claims
run: cargo build -p example-axum-custom-claims
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,10 @@ async fn main() {
.route("/", get(public))
.with_state(FirebaseAuthState { firebase_auth });

let addr = &"127.0.0.1:8080".parse().expect("Cannot parse the addr");
axum::Server::bind(addr)
.serve(app.into_make_service())
.await
.unwrap()
let addr = "127.0.0.1:8080";
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();

axum::serve(listener, app).await.unwrap();
}
```

Expand Down Expand Up @@ -128,6 +127,10 @@ TOKEN="<paste your token here>"
curl --header "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/hello
```

## Firebase Document

[Verify ID tokens using a third-party JWT library](https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library)

## License

[MIT](https://opensource.org/licenses/MIT)
Expand Down
5 changes: 4 additions & 1 deletion examples/actix-basic/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::env;

use actix_web::{get, middleware::Logger, web::Data, App, HttpServer, Responder};
use firebase_auth::{FirebaseAuth, FirebaseUser};

Expand All @@ -14,7 +16,8 @@ async fn public() -> impl Responder {

#[actix_web::main]
async fn main() -> std::io::Result<()> {
let firebase_auth = FirebaseAuth::new("my-project-id").await;
let project_id = env::var("PROJECT_ID").unwrap_or_else(|_| panic!("must set PROJECT_ID"));
let firebase_auth = FirebaseAuth::new(&project_id).await;

let app_data = Data::new(firebase_auth);

Expand Down
13 changes: 13 additions & 0 deletions examples/actix-custom-claims/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "example-actix-custom-claims"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
firebase-auth = { path = "../../firebase-auth" }
actix-web = { version = "4" }
actix-web-httpauth = { version = "0.8.0" }
futures = "0.3"
serde = "1.0"
serde_json = "1.0"
98 changes: 98 additions & 0 deletions examples/actix-custom-claims/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::env;

use actix_web::error::ErrorUnauthorized;
use actix_web::{
dev, get, http::header::Header, middleware::Logger, web, web::Data, App, Error, FromRequest,
HttpRequest, HttpServer, Responder,
};
use actix_web_httpauth::headers::authorization::{Authorization, Bearer};
use firebase_auth::{FirebaseAuth, FirebaseProvider};
use futures::future::{err, ok, Ready};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone)]
pub struct FirebaseUser {
pub iss: String,
pub aud: String,
pub sub: String,
pub iat: u64,
pub exp: u64,
pub auth_time: u64,
pub user_id: String,
pub provider_id: Option<String>,
pub name: Option<String>,
pub picture: Option<String>,
pub email: Option<String>,
pub email_verified: Option<bool>,
pub firebase: FirebaseProvider,

#[serde(rename = "https://hasura.io/jwt/claims")]
pub hasura: HasuraClaims,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct HasuraClaims {
pub x_hasura_default_role: String,
pub x_hasura_allowed_roles: Vec<String>,
pub x_hasura_user_id: String,
}

fn get_bearer_token(header: &str) -> Option<String> {
let prefix_len = "Bearer ".len();

match header.len() {
l if l < prefix_len => None,
_ => Some(header[prefix_len..].to_string()),
}
}

impl FromRequest for FirebaseUser {
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;

fn from_request(req: &HttpRequest, _: &mut dev::Payload) -> Self::Future {
let firebase_auth = req
.app_data::<web::Data<FirebaseAuth>>()
.expect("must init FirebaseAuth in Application Data. see description in https://crates.io/crates/firebase-auth");

let bearer = match Authorization::<Bearer>::parse(req) {
Err(e) => return err(e.into()),
Ok(v) => get_bearer_token(&v.to_string()).unwrap_or_default(),
};

match firebase_auth.verify(&bearer) {
Err(e) => err(ErrorUnauthorized(format!("Failed to verify Token {}", e))),
Ok(user) => ok(user),
}
}
}

#[get("/hello")]
async fn greet(user: FirebaseUser) -> impl Responder {
let hasura_user_id = user.hasura.x_hasura_user_id;
format!("Hello user id {}!", hasura_user_id)
}

#[get("/public")]
async fn public() -> impl Responder {
"ok"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
let project_id = env::var("PROJECT_ID").unwrap_or_else(|_| panic!("must set PROJECT_ID"));
let firebase_auth = FirebaseAuth::new(&project_id).await;

let app_data = Data::new(firebase_auth);

HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.app_data(app_data.clone())
.service(greet)
.service(public)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
14 changes: 14 additions & 0 deletions examples/axum-custom-claims/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "example-axum-custom-claims"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
firebase-auth = { path = "../../firebase-auth" }
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.5.0", features = ["trace"] }
tracing = "0.1"
serde = "1.0"
serde_json = "1.0"
116 changes: 116 additions & 0 deletions examples/axum-custom-claims/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use axum::{
async_trait,
extract::{FromRef, FromRequestParts},
http::{self, request::Parts, StatusCode},
response::{IntoResponse, Response},
routing::get,
Router,
};
use firebase_auth::{FirebaseAuth, FirebaseAuthState, FirebaseProvider};
use serde::{Deserialize, Serialize};
use tracing::debug;

#[derive(Serialize, Deserialize, Clone)]
pub struct FirebaseUser {
pub iss: String,
pub aud: String,
pub sub: String,
pub iat: u64,
pub exp: u64,
pub auth_time: u64,
pub user_id: String,
pub provider_id: Option<String>,
pub name: Option<String>,
pub picture: Option<String>,
pub email: Option<String>,
pub email_verified: Option<bool>,
pub firebase: FirebaseProvider,

#[serde(rename = "https://hasura.io/jwt/claims")]
pub hasura: HasuraClaims,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct HasuraClaims {
pub x_hasura_default_role: String,
pub x_hasura_allowed_roles: Vec<String>,
pub x_hasura_user_id: String,
}

fn get_bearer_token(header: &str) -> Option<String> {
let prefix_len = "Bearer ".len();

match header.len() {
l if l < prefix_len => None,
_ => Some(header[prefix_len..].to_string()),
}
}

#[async_trait]
impl<S> FromRequestParts<S> for FirebaseUser
where
FirebaseAuthState: FromRef<S>,
S: Send + Sync,
{
type Rejection = UnauthorizedResponse;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let store = FirebaseAuthState::from_ref(state);

let auth_header = parts
.headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.unwrap_or("");

let bearer = get_bearer_token(auth_header).map_or(
Err(UnauthorizedResponse {
msg: "Missing Bearer Token".to_string(),
}),
Ok,
)?;

debug!("Got bearer token {}", bearer);

match store.firebase_auth.verify(&bearer) {
Err(e) => Err(UnauthorizedResponse {
msg: format!("Failed to verify Token: {}", e),
}),
Ok(current_user) => Ok(current_user),
}
}
}

pub struct UnauthorizedResponse {
msg: String,
}

impl IntoResponse for UnauthorizedResponse {
fn into_response(self) -> Response {
(StatusCode::UNAUTHORIZED, self.msg).into_response()
}
}

async fn greet(user: FirebaseUser) -> String {
let email = user.email.unwrap_or("empty email".to_string());
format!("hello {}", email)
}

async fn public() -> &'static str {
"ok"
}

#[tokio::main]
async fn main() {
let firebase_auth = FirebaseAuth::new("my-project-id").await;

let app = Router::new()
.route("/hello", get(greet))
.route("/", get(public))
.with_state(FirebaseAuthState { firebase_auth });

let addr = "127.0.0.1:8080";
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();

axum::serve(listener, app).await.unwrap();
}
4 changes: 2 additions & 2 deletions firebase-auth/src/actix_feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ impl FromRequest for FirebaseUser {

fn from_request(req: &HttpRequest, _: &mut dev::Payload) -> Self::Future {
let firebase_auth = req
.app_data::<web::Data<FirebaseAuth>>()
.expect("must init FirebaseAuth in Application Data. see description in https://crates.io/crates/firebase-auth");
.app_data::<web::Data<FirebaseAuth>>()
.expect("must init FirebaseAuth in Application Data. see description in https://crates.io/crates/firebase-auth");

let bearer = match Authorization::<Bearer>::parse(req) {
Err(e) => return err(e.into()),
Expand Down
12 changes: 5 additions & 7 deletions firebase-auth/src/axum_feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,12 @@ where
.and_then(|value| value.to_str().ok())
.unwrap_or("");

let bearer = get_bearer_token(auth_header);
let bearer = if let Some(bearer) = bearer {
bearer
} else {
return Err(UnauthorizedResponse {
let bearer = get_bearer_token(auth_header).map_or(
Err(UnauthorizedResponse {
msg: "Missing Bearer Token".to_string(),
});
};
}),
Ok,
)?;

debug!("Got bearer token {}", bearer);

Expand Down
Loading

0 comments on commit a3a3f5b

Please sign in to comment.