Skip to content

Commit

Permalink
feat(session): add create session cookie feature
Browse files Browse the repository at this point in the history
* Add free standing function sessions::create_session_cookie(credentials, id_token, duration)
* Add example

Signed-off-by: Stephane Eintrazi <[email protected]>
Signed-off-by: David Graeff <[email protected]>
  • Loading branch information
stephane-ein authored and David Graeff committed Jan 23, 2021
1 parent 42e2d17 commit a70727f
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 8 deletions.
11 changes: 11 additions & 0 deletions examples/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ identified by the firebase user id.

* Build and run with `cargo run --example firebase_user`.

## Session cookie example

This example shows how to exchange a ID token, usually given by the firebase web framework on the client side,
into a server-side session cookie.

Firebase Auth provides server-side session cookie management for traditional websites that rely on session cookies.
This solution has several advantages over client-side short-lived ID tokens,
which may require a redirect mechanism each time to update the session cookie on expiration.

* Build and run with `cargo run --example session_cookie`.

## Rocket Protected Route example

[Rocket](https://rocket.rs) is a an easy to use web-framework for Rust.
Expand Down
41 changes: 41 additions & 0 deletions examples/session_cookie.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use firestore_db_and_auth::{
errors::FirebaseError, Credentials, FirebaseAuthBearer, sessions::session_cookie
};

use chrono::Duration;

mod utils;

fn main() -> Result<(), FirebaseError> {
// Search for a credentials file in the root directory
use std::path::PathBuf;

let mut credential_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
credential_file.push("firebase-service-account.json");
let mut cred = Credentials::from_file(credential_file.to_str().unwrap())?;

// Only download the public keys once, and cache them.
let jwkset = utils::from_cache_file(credential_file.with_file_name("cached_jwks.jwks").as_path(), &cred)?;
cred.add_jwks_public_keys(&jwkset);
cred.verify()?;

let cookie = session_cookie::create(&cred, user_session.access_token(), Duration::seconds(3600))?;
println!("Created session cookie: {}", cookie);

Ok(())
}

#[test]
fn create_session_cookie_test() -> Result<(), FirebaseError> {
let cred = utils::valid_test_credentials()?;
let user_session = utils::user_session_with_cached_refresh_token(&cred)?;

assert_eq!(user_session.user_id, utils::TEST_USER_ID);
assert_eq!(user_session.project_id(), cred.project_id);

use chrono::Duration;
let cookie = session_cookie::create(&cred, user_session.access_token(), Duration::seconds(3600))?;

assert!(cookie.len() > 0);
Ok(())
}
58 changes: 56 additions & 2 deletions src/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ pub(crate) fn create_jwt<S>(
user_id: Option<String>,
audience: &str,
) -> Result<AuthClaimsJWT, Error>
where
S: AsRef<str>,
where
S: AsRef<str>,
{
use std::ops::Add;

Expand Down Expand Up @@ -242,3 +242,57 @@ pub(crate) fn verify_access_token(
audience,
})
}

pub mod session_cookie {
use super::*;
use std::ops::Add;

pub(crate) fn create_jwt_encoded(
credentials: &Credentials,
duration: chrono::Duration,
) -> Result<String, Error> {
let scope = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/firebase.database",
"https://www.googleapis.com/auth/firebase.messaging",
"https://www.googleapis.com/auth/identitytoolkit",
"https://www.googleapis.com/auth/userinfo.email",
];

const AUDIENCE: &str = "https://accounts.google.com/o/oauth2/token";

use biscuit::{
jws::{Header, RegisteredHeader},
ClaimsSet, Empty, RegisteredClaims, JWT,
};

let header: Header<Empty> = Header::from(RegisteredHeader {
algorithm: SignatureAlgorithm::RS256,
key_id: Some(credentials.private_key_id.to_owned()),
..Default::default()
});
let expected_claims = ClaimsSet::<JwtOAuthPrivateClaims> {
registered: RegisteredClaims {
issuer: Some(credentials.client_email.clone()),
audience: Some(SingleOrMultiple::Single(AUDIENCE.to_string())),
subject: Some(credentials.client_email.clone()),
expiry: Some(biscuit::Timestamp::from(Utc::now().add(duration))),
issued_at: Some(biscuit::Timestamp::from(Utc::now())),
..Default::default()
},
private: JwtOAuthPrivateClaims {
scope: Some(scope.join(" ")),
client_id: None,
uid: None,
},
};
let jwt = JWT::new_decoded(header, expected_claims);

let secret = credentials
.keys
.secret
.as_ref()
.ok_or(Error::Generic("No private key added via add_keypair_key!"))?;
Ok(jwt.encode(&secret.deref())?.encoded()?.encode())
}
}
109 changes: 103 additions & 6 deletions src/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
use super::credentials;
use super::errors::{extract_google_api_error, FirebaseError};
use super::jwt::{
create_jwt, is_expired, jwt_update_expiry_if, verify_access_token, AuthClaimsJWT, JWT_AUDIENCE_FIRESTORE,
JWT_AUDIENCE_IDENTITY,
create_jwt, is_expired, jwt_update_expiry_if, verify_access_token, AuthClaimsJWT,
JWT_AUDIENCE_FIRESTORE, JWT_AUDIENCE_IDENTITY,
};
use super::FirebaseAuthBearer;

Expand Down Expand Up @@ -58,7 +58,7 @@ pub mod user {
/// Returns the current access token.
/// This method will automatically refresh your access token, if it has expired.
///
/// If the refresh failed, this will
/// If the refresh failed, this will return an empty string.
fn access_token(&self) -> String {
let jwt = self.access_token_.borrow();
let jwt = jwt.as_str();
Expand Down Expand Up @@ -258,12 +258,23 @@ pub mod user {
})
}

pub fn by_access_token(credentials: &Credentials, firebase_tokenid: &str) -> Result<Session, FirebaseError> {
let result = verify_access_token(&credentials, firebase_tokenid)?;
/// Create a new firestore user session by a valid access token
///
/// Remember that such a session cannot renew itself. As soon as the access token expired,
/// no further operations can be issued by this session.
///
/// No network operation is performed, the access token is only checked for its validity.
///
/// Arguments:
/// - `credentials` The credentials
/// - `access_token` An access token, sometimes called a firebase id token.
///
pub fn by_access_token(credentials: &Credentials, access_token: &str) -> Result<Session, FirebaseError> {
let result = verify_access_token(&credentials, access_token)?;
Ok(Session {
user_id: result.subject,
project_id_: result.audience,
access_token_: RefCell::new(firebase_tokenid.to_owned()),
access_token_: RefCell::new(access_token.to_owned()),
refresh_token: None,
api_key: credentials.api_key.clone(),
client: reqwest::blocking::Client::new(),
Expand All @@ -273,6 +284,92 @@ pub mod user {
}
}

pub mod session_cookie {
use super::*;

pub static GOOGLE_OAUTH2_URL: &str = "https://accounts.google.com/o/oauth2/token";

/// See https://cloud.google.com/identity-platform/docs/reference/rest/v1/projects/createSessionCookie
#[inline]
fn identitytoolkit_url(project_id: &str) -> String {
format!("https://identitytoolkit.googleapis.com/v1/projects/{}:createSessionCookie", project_id)
}

/// See https://cloud.google.com/identity-platform/docs/reference/rest/v1/CreateSessionCookieResponse
#[derive(Debug, Deserialize)]
struct CreateSessionCookieResponseDTO {
#[serde(rename = "sessionCookie")]
session_cookie_jwk: String,
}

/// https://cloud.google.com/identity-platform/docs/reference/rest/v1/projects/createSessionCookie
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
struct SessionLoginDTO {
/// Required. A valid Identity Platform ID token.
#[serde(rename = "idToken")]
id_token: String,
/// The number of seconds until the session cookie expires. Specify a duration in seconds, between five minutes and fourteen days, inclusively.
#[serde(rename = "validDuration")]
valid_duration: u64,
#[serde(rename = "tenantId")]
#[serde(skip_serializing_if = "Option::is_none")]
tenant_id: Option<String>,
}

#[derive(Debug, Deserialize)]
struct Oauth2ResponseDTO {
access_token: String,
expires_in: u64,
token_type: String,
}

/// Firebase Auth provides server-side session cookie management for traditional websites that rely on session cookies.
/// This solution has several advantages over client-side short-lived ID tokens,
/// which may require a redirect mechanism each time to update the session cookie on expiration:
///
/// * Improved security via JWT-based session tokens that can only be generated using authorized service accounts.
/// * Stateless session cookies that come with all the benefit of using JWTs for authentication.
/// The session cookie has the same claims (including custom claims) as the ID token, making the same permissions checks enforceable on the session cookies.
/// * Ability to create session cookies with custom expiration times ranging from 5 minutes to 2 weeks.
/// * Flexibility to enforce cookie policies based on application requirements: domain, path, secure, httpOnly, etc.
/// * Ability to revoke session cookies when token theft is suspected using the existing refresh token revocation API.
/// * Ability to detect session revocation on major account changes.
///
/// See https://firebase.google.com/docs/auth/admin/manage-cookies
///
/// The generated session cookie is a JWT that includes the firebase user id in the "sub" (subject) field.
///
/// Arguments:
/// - `credentials` The credentials
/// - `id_token` An access token, sometimes called a firebase id token.
/// - `duration` The cookie duration
///
pub fn create(credentials: &credentials::Credentials, id_token: String, duration: chrono::Duration) -> Result<String, FirebaseError> {
// Generate the assertion from the admin credentials
let assertion = crate::jwt::session_cookie::create_jwt_encoded(credentials, duration)?;

// Request Google Oauth2 to retrieve the access token in order to create a session cookie
let client = reqwest::blocking::Client::new();
let response_oauth2: Oauth2ResponseDTO = client.post(GOOGLE_OAUTH2_URL)
.form(&[("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"), ("assertion", &assertion)])
.send()?
.json()?;

// Create a session cookie with the access token previously retrieved
let response_session_cookie_json: CreateSessionCookieResponseDTO = client.post(&identitytoolkit_url(&credentials.project_id))
.bearer_auth(&response_oauth2.access_token)
.json(&SessionLoginDTO {
id_token,
valid_duration: duration.num_seconds() as u64,
tenant_id: None,
})
.send()?
.json()?;

Ok(response_session_cookie_json.session_cookie_jwk)
}
}

/// Find the service account session defined in here
pub mod service_account {
use super::*;
Expand Down

0 comments on commit a70727f

Please sign in to comment.