Skip to content

Commit

Permalink
Jwt (#12)
Browse files Browse the repository at this point in the history
* oatuh jwt

* oatuh jwt

* jwt basic

* jwt assertion

* jwt token

* jwt token

* added JWT_ENV
  • Loading branch information
barduinor authored Sep 4, 2023
1 parent 089c552 commit c88f80b
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/ci_orig.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ jobs:
run: |
echo "${{secrets.CCG_ENV}}"
echo "${{secrets.CCG_ENV}}" | base64 -d > .ccg.env
- name: Create .jwt.env from secrets
run: |
echo "${{secrets.JWT_ENV}}"
echo "${{secrets.JWT_ENV}}" | base64 -d > .jwt.env
- name: Build sources
uses: actions-rs/cargo@v1
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ chrono = { version = "0.4.24", features = ["serde"] }
async-trait = "0.1.68"
url = "2.2.2"
rand = "0.8.5"
josekit = "0.8.3"
openssl = "0.10.57"
uuid = { version = "1.4.1", features = ["v4", "fast-rng","macro-diagnostics"]}

[dev-dependencies]
pretty_assertions = "1.3.0"
Expand Down
56 changes: 56 additions & 0 deletions examples/auth_jwt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// use cargo run --example users_main to run this file
// use dotenv;

use rusty_box::{
auth::auth_jwt::JWTAuth,
auth::auth_jwt::SubjectType,
client::{box_client::BoxClient, client_error::BoxAPIError},
config::Config,
rest_api::users::users_api,
};
use std::env;

#[tokio::main]
async fn main() -> Result<(), BoxAPIError> {
dotenv::from_filename(".jwt.env").ok();

let client_id = env::var("CLIENT_ID").expect("CLIENT_ID must be set");
let client_secret = env::var("CLIENT_SECRET").expect("CLIENT_SECRET must be set");

let env_subject_type = env::var("BOX_SUBJECT_TYPE").expect("BOX_SUBJECT_TYPE must be set");
let box_subject_type = match env_subject_type.as_str() {
"user" => SubjectType::User,
"enterprise" => SubjectType::Enterprise,
_ => panic!("BOX_SUBJECT_TYPE must be either 'user' or 'enterprise'"),
};

let box_subject_id = env::var("BOX_SUBJECT_ID").expect("BOX_SUBJECT_ID must be set");

let public_key_id = env::var("PUBLIC_KEY_ID").expect("PUBLIC_KEY_ID must be set");

let encrypted_private_key =
env::var("ENCRYPTED_PRIVATE_KEY").expect("ENCRYPTED_PRIVATE_KEY must be set");

let passphrase = env::var("PASSPHRASE").expect("PASSPHRASE must be set");

let config = Config::new();
let auth = JWTAuth::new(
config,
client_id,
client_secret,
box_subject_type,
box_subject_id,
public_key_id,
encrypted_private_key,
passphrase,
);

let mut client = BoxClient::new(Box::new(auth));

let fields = vec![];

let me = users_api::me(&mut client, Some(fields)).await?;
println!("Me:\n{me:#?}\n");

Ok(())
}
1 change: 1 addition & 0 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod auth_ccg;
pub mod auth_client;
pub mod auth_developer;
pub mod auth_errors;
pub mod auth_jwt;
pub mod auth_oauth;

use async_trait::async_trait;
Expand Down
18 changes: 18 additions & 0 deletions src/auth/auth_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub enum AuthError {
Io(std::io::Error),
Generic(String),
ResponseError(AuthErrorResponse),
JoseError(josekit::JoseError),
OpenSSl(openssl::error::ErrorStack),
}

impl fmt::Display for AuthError {
Expand All @@ -48,6 +50,8 @@ impl fmt::Display for AuthError {
AuthError::Io(e) => ("IO", e.to_string()),
AuthError::Generic(e) => ("Token", e.to_string()),
AuthError::ResponseError(e) => ("API Error", e.to_string()),
AuthError::JoseError(e) => ("Jose", e.to_string()),
AuthError::OpenSSl(e) => ("OpenSSL", e.to_string()),
};
write!(f, "error in {}: {}", module, e)
}
Expand All @@ -61,6 +65,8 @@ impl fmt::Debug for AuthError {
AuthError::Io(e) => ("IO", e.to_string()),
AuthError::Generic(e) => ("Token", e.to_string()),
AuthError::ResponseError(e) => ("API Error", e.to_string()),
AuthError::JoseError(e) => ("Jose", e.to_string()),
AuthError::OpenSSl(e) => ("OpenSSL", e.to_string()),
};
write!(f, "error in {}: {}", module, e)
}
Expand Down Expand Up @@ -95,3 +101,15 @@ impl From<AuthErrorResponse> for AuthError {
AuthError::ResponseError(e)
}
}

impl From<josekit::JoseError> for AuthError {
fn from(e: josekit::JoseError) -> Self {
AuthError::JoseError(e)
}
}

impl From<openssl::error::ErrorStack> for AuthError {
fn from(e: openssl::error::ErrorStack) -> Self {
AuthError::OpenSSl(e)
}
}
205 changes: 205 additions & 0 deletions src/auth/auth_jwt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//! JSON Web Token (JWT) authentication
use super::access_token::AccessToken;
use super::auth_client::{AuthClient, Form};
use super::{Auth, AuthError};
use crate::config::Config;

use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use josekit::{
jws::RS512,
jwt::{self, JwtPayload},
};
use openssl::pkey::PKey;
use serde::Serialize;
use serde_json::json;

/// The type of subject that is being authenticated (user or enterprise)
#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum SubjectType {
Enterprise,
User,
}
impl SubjectType {
fn value(&self) -> String {
match self {
Self::Enterprise => "enterprise".to_owned(),
Self::User => "user".to_owned(),
}
}
}
impl Default for SubjectType {
fn default() -> SubjectType {
Self::Enterprise
}
}

#[derive(Debug, Clone, Serialize, Default)]
pub struct JWTAuth {
pub config: Config,
client_id: String,
client_secret: String,

box_subject_type: SubjectType,
box_subject_id: String,

public_key_id: String,
#[serde(skip)]
private_key: String,
#[serde(skip)]
passphrase: String,
access_token: AccessToken,
expires_by: DateTime<Utc>,
#[serde(skip)]
client: AuthClient,
}

impl JWTAuth {
#[allow(clippy::too_many_arguments)]
pub fn new(
config: Config,
client_id: String,
client_secret: String,

box_subject_type: SubjectType,
box_subject_id: String,

public_key_id: String,
private_key: String,
passphrase: String,
) -> Self {
JWTAuth {
config,
client_id,
client_secret,

box_subject_type,
box_subject_id,

public_key_id,
private_key,
passphrase,

access_token: AccessToken::new(),
expires_by: Utc::now(),
client: AuthClient::default(),
}
}

pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_by - Duration::seconds(60 * 5)
}

async fn fetch_access_token(&mut self) -> Result<AccessToken, AuthError> {
let url = &(self.config.oauth2_api_url.clone() + "/token");

let headers = None; // TODO: Add headers to rquest

let jwt_token = jwt_assertion(self.clone())?;

let mut payload = Form::new();
payload.insert("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
payload.insert("client_id", &self.client_id);
payload.insert("client_secret", &self.client_secret);

payload.insert("assertion", &jwt_token);

let now = Utc::now();

let response = self.client.post_form(url, headers, &payload).await;

let data = match response {
Ok(data) => data,
Err(e) => return Err(e),
};

let access_token = match serde_json::from_str::<AccessToken>(&data) {
Ok(access_token) => access_token,
Err(e) => {
return Err(AuthError::Serde(e));
}
};
let expires_in = access_token.expires_in.unwrap_or_default();
self.expires_by = now + Duration::seconds(expires_in);
self.access_token = access_token.clone();
Ok(access_token)
}
}

fn jwt_assertion(jwt_aut: JWTAuth) -> Result<String, AuthError> {
// JWT Header
let mut header = josekit::jws::JwsHeader::new();
header.set_token_type("JWT");

header.set_key_id(jwt_aut.public_key_id);

// JWT Payload
let mut payload = JwtPayload::new();

payload.set_issuer(jwt_aut.client_id);

payload.set_subject(jwt_aut.box_subject_id);

let box_subject_type = Some(json!(jwt_aut.box_subject_type.value()));
payload.set_claim("box_sub_type", box_subject_type)?;

let audience = vec![jwt_aut.config.oauth2_api_url + "/token"];
payload.set_audience(audience);

let jwt_id = uuid::Uuid::new_v4().to_string();
payload.set_jwt_id(jwt_id);

let expires_at = std::time::SystemTime::now() + std::time::Duration::from_secs(59);
payload.set_expires_at(&expires_at);

// decrupt private key
let private_key = PKey::private_key_from_pem_passphrase(
jwt_aut.private_key.as_bytes(),
jwt_aut.passphrase.as_bytes(),
)?;

let private_key_pem = private_key.private_key_to_pem_pkcs8()?;

let signer = RS512.signer_from_pem(private_key_pem)?;
let jwt = jwt::encode_with_signer(&payload, &header, &signer)?;

Ok(jwt)
}

#[async_trait]
impl<'a> Auth<'a> for JWTAuth {
async fn access_token(&mut self) -> Result<String, AuthError> {
if self.is_expired() {
match self.fetch_access_token().await {
Ok(access_token) => Ok(access_token.access_token.unwrap_or_default()),
Err(e) => Err(e),
}
} else {
let access_token = match self.access_token.access_token.clone() {
Some(token) => token,
None => return Err(AuthError::Generic("CCG token is not set".to_owned())),
};
Ok(access_token)
}
}

async fn to_json(&mut self) -> Result<String, AuthError> {
self.access_token().await?;
match serde_json::to_string(&self) {
Ok(json) => Ok(json),
Err(e) => Err(AuthError::Serde(e)),
}
}

fn base_api_url(&self) -> String {
self.config.base_api_url()
}

fn user_agent(&self) -> String {
self.config.user_agent()
}
}

#[cfg(test)]
#[path = "./unit_tests/auth_jwt_test.rs"]
mod auth_jwt_test;
57 changes: 57 additions & 0 deletions src/auth/unit_tests/auth_jwt_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::config;

use super::*;

#[test]
fn test_jwt_assertion() {
// should create a Box JWT assertion
let config = config::Config::default();

// print current dir
println!("Current dir: {:?}", std::env::current_dir());

let encrypted_private_key = std::fs::read_to_string("tests/keys/private.pem").unwrap();
let public_key = std::fs::read_to_string("tests/keys/public.pem").unwrap();
let passphrase = "ABCD".to_string();

let jwt_auth = JWTAuth::new(
config,
"client_id".to_string(),
"client_secret".to_string(),
SubjectType::Enterprise,
"box_subject_id".to_string(),
"public_key_id".to_string(),
encrypted_private_key,
passphrase,
);

let jwt = jwt_assertion(jwt_auth).unwrap_or("".to_string());

// have a non empty jwt
assert!(jwt.len() > 0);

//have 3 dots in assertion
let count = jwt.chars().filter(|&c| c == '.').count();
assert_eq!(count, 2);

// have a valid signature
// Verifing JWT
let verifier = RS512.verifier_from_pem(&public_key).unwrap();
let (payload, header) = jwt::decode_with_verifier(&jwt, &verifier).unwrap();

// assert headers
assert_eq!(header.token_type(), Some("JWT"));
assert_eq!(header.key_id(), Some("public_key_id"));
assert_eq!(header.algorithm(), Some("RS512"));

//assert claims
assert_eq!(payload.issuer(), Some("client_id"));
assert_eq!(payload.subject(), Some("box_subject_id"));
assert_eq!(payload.claim("box_sub_type"), Some(&json!("enterprise")));
assert_eq!(
payload.audience(),
Some(vec!["https://api.box.com/oauth2/token"])
);
assert!(payload.jwt_id() != Some(""));
assert!(payload.expires_at() > Some(std::time::SystemTime::now()));
}
Loading

0 comments on commit c88f80b

Please sign in to comment.