Skip to content

Commit d7d3e45

Browse files
stephane-eindavidgraeff
authored andcommitted
feat(session): add create session cookie feature
* 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]>
1 parent 0f2c22d commit d7d3e45

File tree

4 files changed

+216
-4
lines changed

4 files changed

+216
-4
lines changed

examples/readme.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ identified by the firebase user id.
2323

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

26+
## Session cookie example
27+
28+
This example shows how to exchange a ID token, usually given by the firebase web framework on the client side,
29+
into a server-side session cookie.
30+
31+
Firebase Auth provides server-side session cookie management for traditional websites that rely on session cookies.
32+
This solution has several advantages over client-side short-lived ID tokens,
33+
which may require a redirect mechanism each time to update the session cookie on expiration.
34+
35+
* Build and run with `cargo run --example session_cookie`.
36+
2637
## Rocket Protected Route example
2738

2839
[Rocket](https://rocket.rs) is a an easy to use web-framework for Rust.

examples/session_cookie.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use firestore_db_and_auth::{errors::FirebaseError, sessions::session_cookie, Credentials, FirebaseAuthBearer};
2+
3+
use chrono::Duration;
4+
5+
mod utils;
6+
7+
fn main() -> Result<(), FirebaseError> {
8+
// Search for a credentials file in the root directory
9+
use std::path::PathBuf;
10+
11+
let mut credential_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
12+
credential_file.push("firebase-service-account.json");
13+
let mut cred = Credentials::from_file(credential_file.to_str().unwrap())?;
14+
15+
// Only download the public keys once, and cache them.
16+
let jwkset = utils::from_cache_file(credential_file.with_file_name("cached_jwks.jwks").as_path(), &cred)?;
17+
cred.add_jwks_public_keys(&jwkset);
18+
cred.verify()?;
19+
20+
let user_session = utils::user_session_with_cached_refresh_token(&cred)?;
21+
22+
let cookie = session_cookie::create(&cred, user_session.access_token(), Duration::seconds(3600))?;
23+
println!("Created session cookie: {}", cookie);
24+
25+
Ok(())
26+
}
27+
28+
#[test]
29+
fn create_session_cookie_test() -> Result<(), FirebaseError> {
30+
let cred = utils::valid_test_credentials()?;
31+
let user_session = utils::user_session_with_cached_refresh_token(&cred)?;
32+
33+
assert_eq!(user_session.user_id, utils::TEST_USER_ID);
34+
assert_eq!(user_session.project_id(), cred.project_id);
35+
36+
use chrono::Duration;
37+
let cookie = session_cookie::create(&cred, user_session.access_token(), Duration::seconds(3600))?;
38+
39+
assert!(cookie.len() > 0);
40+
Ok(())
41+
}

src/jwt.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,54 @@ pub(crate) fn verify_access_token(
242242
audience,
243243
})
244244
}
245+
246+
pub mod session_cookie {
247+
use super::*;
248+
use std::ops::Add;
249+
250+
pub(crate) fn create_jwt_encoded(credentials: &Credentials, duration: chrono::Duration) -> Result<String, Error> {
251+
let scope = [
252+
"https://www.googleapis.com/auth/cloud-platform",
253+
"https://www.googleapis.com/auth/firebase.database",
254+
"https://www.googleapis.com/auth/firebase.messaging",
255+
"https://www.googleapis.com/auth/identitytoolkit",
256+
"https://www.googleapis.com/auth/userinfo.email",
257+
];
258+
259+
const AUDIENCE: &str = "https://accounts.google.com/o/oauth2/token";
260+
261+
use biscuit::{
262+
jws::{Header, RegisteredHeader},
263+
ClaimsSet, Empty, RegisteredClaims, JWT,
264+
};
265+
266+
let header: Header<Empty> = Header::from(RegisteredHeader {
267+
algorithm: SignatureAlgorithm::RS256,
268+
key_id: Some(credentials.private_key_id.to_owned()),
269+
..Default::default()
270+
});
271+
let expected_claims = ClaimsSet::<JwtOAuthPrivateClaims> {
272+
registered: RegisteredClaims {
273+
issuer: Some(credentials.client_email.clone()),
274+
audience: Some(SingleOrMultiple::Single(AUDIENCE.to_string())),
275+
subject: Some(credentials.client_email.clone()),
276+
expiry: Some(biscuit::Timestamp::from(Utc::now().add(duration))),
277+
issued_at: Some(biscuit::Timestamp::from(Utc::now())),
278+
..Default::default()
279+
},
280+
private: JwtOAuthPrivateClaims {
281+
scope: Some(scope.join(" ")),
282+
client_id: None,
283+
uid: None,
284+
},
285+
};
286+
let jwt = JWT::new_decoded(header, expected_claims);
287+
288+
let secret = credentials
289+
.keys
290+
.secret
291+
.as_ref()
292+
.ok_or(Error::Generic("No private key added via add_keypair_key!"))?;
293+
Ok(jwt.encode(&secret.deref())?.encoded()?.encode())
294+
}
295+
}

src/sessions.rs

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub mod user {
5858
/// Returns the current access token.
5959
/// This method will automatically refresh your access token, if it has expired.
6060
///
61-
/// If the refresh failed, this will
61+
/// If the refresh failed, this will return an empty string.
6262
fn access_token(&self) -> String {
6363
let jwt = self.access_token_.borrow();
6464
let jwt = jwt.as_str();
@@ -258,12 +258,23 @@ pub mod user {
258258
})
259259
}
260260

261-
pub fn by_access_token(credentials: &Credentials, firebase_tokenid: &str) -> Result<Session, FirebaseError> {
262-
let result = verify_access_token(&credentials, firebase_tokenid)?;
261+
/// Create a new firestore user session by a valid access token
262+
///
263+
/// Remember that such a session cannot renew itself. As soon as the access token expired,
264+
/// no further operations can be issued by this session.
265+
///
266+
/// No network operation is performed, the access token is only checked for its validity.
267+
///
268+
/// Arguments:
269+
/// - `credentials` The credentials
270+
/// - `access_token` An access token, sometimes called a firebase id token.
271+
///
272+
pub fn by_access_token(credentials: &Credentials, access_token: &str) -> Result<Session, FirebaseError> {
273+
let result = verify_access_token(&credentials, access_token)?;
263274
Ok(Session {
264275
user_id: result.subject,
265276
project_id_: result.audience,
266-
access_token_: RefCell::new(firebase_tokenid.to_owned()),
277+
access_token_: RefCell::new(access_token.to_owned()),
267278
refresh_token: None,
268279
api_key: credentials.api_key.clone(),
269280
client: reqwest::blocking::Client::new(),
@@ -273,6 +284,104 @@ pub mod user {
273284
}
274285
}
275286

287+
pub mod session_cookie {
288+
use super::*;
289+
290+
pub static GOOGLE_OAUTH2_URL: &str = "https://accounts.google.com/o/oauth2/token";
291+
292+
/// See https://cloud.google.com/identity-platform/docs/reference/rest/v1/projects/createSessionCookie
293+
#[inline]
294+
fn identitytoolkit_url(project_id: &str) -> String {
295+
format!(
296+
"https://identitytoolkit.googleapis.com/v1/projects/{}:createSessionCookie",
297+
project_id
298+
)
299+
}
300+
301+
/// See https://cloud.google.com/identity-platform/docs/reference/rest/v1/CreateSessionCookieResponse
302+
#[derive(Debug, Deserialize)]
303+
struct CreateSessionCookieResponseDTO {
304+
#[serde(rename = "sessionCookie")]
305+
session_cookie_jwk: String,
306+
}
307+
308+
/// https://cloud.google.com/identity-platform/docs/reference/rest/v1/projects/createSessionCookie
309+
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
310+
struct SessionLoginDTO {
311+
/// Required. A valid Identity Platform ID token.
312+
#[serde(rename = "idToken")]
313+
id_token: String,
314+
/// The number of seconds until the session cookie expires. Specify a duration in seconds, between five minutes and fourteen days, inclusively.
315+
#[serde(rename = "validDuration")]
316+
valid_duration: u64,
317+
#[serde(rename = "tenantId")]
318+
#[serde(skip_serializing_if = "Option::is_none")]
319+
tenant_id: Option<String>,
320+
}
321+
322+
#[derive(Debug, Deserialize)]
323+
struct Oauth2ResponseDTO {
324+
access_token: String,
325+
expires_in: u64,
326+
token_type: String,
327+
}
328+
329+
/// Firebase Auth provides server-side session cookie management for traditional websites that rely on session cookies.
330+
/// This solution has several advantages over client-side short-lived ID tokens,
331+
/// which may require a redirect mechanism each time to update the session cookie on expiration:
332+
///
333+
/// * Improved security via JWT-based session tokens that can only be generated using authorized service accounts.
334+
/// * Stateless session cookies that come with all the benefit of using JWTs for authentication.
335+
/// The session cookie has the same claims (including custom claims) as the ID token, making the same permissions checks enforceable on the session cookies.
336+
/// * Ability to create session cookies with custom expiration times ranging from 5 minutes to 2 weeks.
337+
/// * Flexibility to enforce cookie policies based on application requirements: domain, path, secure, httpOnly, etc.
338+
/// * Ability to revoke session cookies when token theft is suspected using the existing refresh token revocation API.
339+
/// * Ability to detect session revocation on major account changes.
340+
///
341+
/// See https://firebase.google.com/docs/auth/admin/manage-cookies
342+
///
343+
/// The generated session cookie is a JWT that includes the firebase user id in the "sub" (subject) field.
344+
///
345+
/// Arguments:
346+
/// - `credentials` The credentials
347+
/// - `id_token` An access token, sometimes called a firebase id token.
348+
/// - `duration` The cookie duration
349+
///
350+
pub fn create(
351+
credentials: &credentials::Credentials,
352+
id_token: String,
353+
duration: chrono::Duration,
354+
) -> Result<String, FirebaseError> {
355+
// Generate the assertion from the admin credentials
356+
let assertion = crate::jwt::session_cookie::create_jwt_encoded(credentials, duration)?;
357+
358+
// Request Google Oauth2 to retrieve the access token in order to create a session cookie
359+
let client = reqwest::blocking::Client::new();
360+
let response_oauth2: Oauth2ResponseDTO = client
361+
.post(GOOGLE_OAUTH2_URL)
362+
.form(&[
363+
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
364+
("assertion", &assertion),
365+
])
366+
.send()?
367+
.json()?;
368+
369+
// Create a session cookie with the access token previously retrieved
370+
let response_session_cookie_json: CreateSessionCookieResponseDTO = client
371+
.post(&identitytoolkit_url(&credentials.project_id))
372+
.bearer_auth(&response_oauth2.access_token)
373+
.json(&SessionLoginDTO {
374+
id_token,
375+
valid_duration: duration.num_seconds() as u64,
376+
tenant_id: None,
377+
})
378+
.send()?
379+
.json()?;
380+
381+
Ok(response_session_cookie_json.session_cookie_jwk)
382+
}
383+
}
384+
276385
/// Find the service account session defined in here
277386
pub mod service_account {
278387
use super::*;

0 commit comments

Comments
 (0)