Skip to content

Commit

Permalink
Implement jwk
Browse files Browse the repository at this point in the history
  • Loading branch information
xerbalind committed Oct 30, 2023
1 parent f00d6e1 commit 6fde689
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 16 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ parking_lot = { version = "0.12" }
thiserror = "1.0"
validator = { version = "0.16", features = [ "derive" ] }
jsonwebtoken = "9.1"
openssl = "0.10"
3 changes: 2 additions & 1 deletion Rocket.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ port = 8000
# Values you want to fill in for production use
# admin_email = # Email address to send admin notifications to (e.g. [email protected])
# secret_key = # used to encrypt cookies (generate a new one!)
# ec_private_key = # Path to ECDSA private key for signing jwt's (generate by running following command: openssl ecparam -genkey -noout -name prime256v1 | openssl pkcs8 -topk8 -nocrypt -out ec-private.pem)
# ec_private_key = # Path to ECDSA private key for signing jwt's. Key Algo needs to be ES384 in PKCS#8 form.
# generate by running: openssl ecparam -genkey -noout -name secp384r1 | openssl pkcs8 -topk8 -nocrypt -out ec-private.pem)
# base_url = # URL where the application is hosten (e.g. https://auth.zeus.gent)
# mail_from = # From header to set when sending emails (e.g. [email protected])
# mail_server = # domain of the SMTP server used to send mail (e.g. smtp.zeus.gent)
Expand Down
7 changes: 4 additions & 3 deletions keys/replace_me.pem
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgSuLKXAj/USilT19q
9w/zvWagD22zLZAVsIuHxCvNFJahRANCAASFlkcs08tC9T2bdhQeqfaR/ZHSXAqG
mHFgziyBAi/TfQ71y8uTZv7+pL/eI18Iz5hQhCvEOxA2GDxN20LQAoFS
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDNQu50efpQZtGKN1tx
j5h+/br9yPUFc5gcvGqQd9wXGa1t8bW/LxtZ/Ho/yPALTIihZANiAARUWV9grHuS
RSVYlanDOaWyrIRbmwbWwJnL6InJoZwGNSEeTmK15H3QgeMA+KF3+yDkw2ECXEtS
7gyURyrAzUOK59QACUMgRuRsP7vUGq5/nMJFSLsb+reiKAmB7G/fUxE=
-----END PRIVATE KEY-----
1 change: 0 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ impl Config {
pub fn base_url(&self) -> Absolute<'_> {
Absolute::parse(&self.base_url).expect("valid base_url")
}

}

pub struct AdminEmail(pub Mailbox);
7 changes: 7 additions & 0 deletions src/controllers/oauth_controller.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use jsonwebtoken::jwk::JwkSet;
use rocket::form::Form;
use rocket::http::{Cookie, CookieJar};
use rocket::response::{Redirect, Responder};
Expand Down Expand Up @@ -243,6 +244,7 @@ fn authorization_denied(state: AuthState) -> Redirect {
pub struct TokenSuccess {
access_token: String,
token_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
id_token: Option<String>,
expires_in: i64,
}
Expand Down Expand Up @@ -340,3 +342,8 @@ pub async fn token(
}))
}
}

#[get("/oauth/jwks")]
pub async fn jwks(jwt_builder: &State<JWTBuilder>) -> Json<JwkSet> {
Json(jwt_builder.jwks.clone())
}
75 changes: 65 additions & 10 deletions src/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ use crate::errors::{InternalError, LaunchError, Result};
use crate::models::client::Client;
use crate::models::user::User;
use chrono::Utc;
use jsonwebtoken::jwk::{
CommonParameters, EllipticCurveKeyParameters, Jwk, JwkSet,
};
use jsonwebtoken::{encode, EncodingKey, Header};
use openssl::bn::{BigNum, BigNumContext};
use openssl::ec::EcKey;
use serde::Serialize;
use std::fs::File;
use std::io::Read;

pub struct JWTBuilder {
pub key: EncodingKey,
pub header: Header,
pub jwks: JwkSet,
}

#[derive(Serialize, Debug)]
pub struct IDToken {
struct IDToken {
sub: String,
iss: String,
aud: String,
Expand All @@ -34,9 +40,57 @@ impl JWTBuilder {

let key = EncodingKey::from_ec_pem(&buffer)
.map_err(|err| LaunchError::BadConfigValueType(err.to_string()))?;
let header = Header::new(jsonwebtoken::Algorithm::ES256);
let header = Header::new(jsonwebtoken::Algorithm::ES384);

Ok(JWTBuilder { key, header })
let private_key = EcKey::private_key_from_pem(&buffer)
.map_err(|err| LaunchError::BadConfigValueType(err.to_string()))?;

let mut ctx: BigNumContext = BigNumContext::new().unwrap();
let public_key = private_key.public_key();
let mut x = BigNum::new().unwrap();
let mut y = BigNum::new().unwrap();
public_key
.affine_coordinates(private_key.group(), &mut x, &mut y, &mut ctx)
.expect("x,y coordinates");

let jwk = Jwk {
common: CommonParameters {
public_key_use: Some(
jsonwebtoken::jwk::PublicKeyUse::Signature,
),
key_algorithm: Some(
jsonwebtoken::jwk::KeyAlgorithm::ES384,
),
key_operations: None,
key_id: None,
x509_url: None,
x509_chain: None,
x509_sha1_fingerprint: None,
x509_sha256_fingerprint: None,
},
algorithm: jsonwebtoken::jwk::AlgorithmParameters::EllipticCurve(
EllipticCurveKeyParameters {
key_type: jsonwebtoken::jwk::EllipticCurveKeyType::EC,
curve: jsonwebtoken::jwk::EllipticCurve::P384,
x: base64::encode_config(
x.to_vec(),
base64::URL_SAFE_NO_PAD,
),
y: base64::encode_config(
y.to_vec(),
base64::URL_SAFE_NO_PAD,
),
},
),
};

Ok(JWTBuilder {
key,
header,
jwks: JwkSet {
keys: Vec::from([jwk]),
},
})
}

pub fn encode<T: Serialize>(&self, claims: &T) -> Result<String> {
Expand All @@ -51,13 +105,14 @@ impl JWTBuilder {
config: &Config,
) -> Result<String> {
let id_token = IDToken {
sub: user.id.to_string(),
iss: config.base_url().to_string(),
aud: client.name.clone(),
iat: Utc::now().timestamp(),
exp: Utc::now().timestamp() + config.client_session_seconds,
nickname: user.username.clone(),
email: user.email.clone(),
sub: user.id.to_string(),
iss: config.base_url().to_string(),
aud: client.name.clone(),
iat: Utc::now().timestamp(),
exp: Utc::now().timestamp()
+ config.client_session_seconds,
preferred_username: user.username.clone(),
email: user.email.clone(),
};
self.encode(&id_token)
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ pub mod db_seed;
pub mod ephemeral;
pub mod errors;
pub mod http_authentication;
pub mod jwt;
pub mod mailer;
pub mod models;
pub mod token_store;
pub mod jwt;
pub mod util;

use diesel_migrations::MigrationHarness;
Expand Down Expand Up @@ -111,6 +111,7 @@ fn assemble(rocket: Rocket<Build>) -> Rocket<Build> {
oauth_controller::grant_get,
oauth_controller::grant_post,
oauth_controller::token,
oauth_controller::jwks,
pages_controller::home_page,
sessions_controller::create_session,
sessions_controller::new_session,
Expand Down
2 changes: 2 additions & 0 deletions tests/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ async fn normal_flow() {
dbg!(&data);
assert!(data["access_token"].is_string());
assert!(data["token_type"].is_string());
assert_eq!(data.get("id_token"), None);
assert_eq!(data["token_type"], "bearer");

// 7b. Client requests access code while sending its credentials
Expand Down Expand Up @@ -255,6 +256,7 @@ async fn normal_flow() {
serde_json::from_str(&response_body).expect("response json values");

assert!(data["access_token"].is_string());
assert_eq!(data["id_token"], Value::Null);
assert_eq!(data["token_type"], "bearer");
let token = data["access_token"].as_str().expect("access token");

Expand Down

0 comments on commit 6fde689

Please sign in to comment.