Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Openid Connect implementation #262

Merged
merged 5 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ db/*
.envrc
static/dist/
node_modules/
keys/*.pem
79 changes: 79 additions & 0 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ tempfile = "3.1"
parking_lot = { version = "0.12" }
thiserror = "1.0"
validator = { version = "0.16", features = [ "derive" ] }
jsonwebtoken = "9.1"
openssl = "0.10"

[build-dependencies]
openssl = "0.10"
3 changes: 3 additions & 0 deletions Rocket.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ maximum_pending_users = 25

[debug]
secret_key = "1vwCFFPSdQya895gNiO556SzmfShG6MokstgttLvwjw="
ec_private_key = "keys/jwt_key.pem"
bcrypt_cost = 4
seed_database = true

Expand All @@ -29,6 +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. 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
18 changes: 18 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use std::fs::File;
use std::io::Write;
use std::path::Path;

use openssl::ec::{EcGroup, EcKey};
use openssl::nid::Nid;
use openssl::pkey::PKey;

fn main() {
let path = Path::new("keys/jwt_key.pem");
if !path.exists() {
let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap();
let pkey = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap();
let mut f = File::create(path).unwrap();
let pem = pkey.private_key_to_pem_pkcs8().unwrap();
f.write_all(&pem).unwrap();
}
}
Empty file added keys/.gitkeep
Empty file.
1 change: 1 addition & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct Config {
pub secure_token_length: usize,
pub bcrypt_cost: u32,
pub base_url: String,
pub ec_private_key: String,
pub mail_queue_size: usize,
pub mail_queue_wait_seconds: u64,
pub mail_from: String,
Expand Down
41 changes: 36 additions & 5 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 All @@ -10,6 +11,7 @@ use crate::ephemeral::session::UserSession;
use crate::errors::Either::{Left, Right};
use crate::errors::*;
use crate::http_authentication::BasicAuthentication;
use crate::jwt::JWTBuilder;
use crate::models::client::*;
use crate::models::session::*;
use crate::models::user::*;
Expand Down Expand Up @@ -161,6 +163,7 @@ pub struct UserToken {
pub client_id: i32,
pub client_name: String,
pub redirect_uri: String,
pub scope: Option<String>,
}

#[get("/oauth/grant")]
Expand Down Expand Up @@ -215,6 +218,7 @@ async fn authorization_granted(
let authorization_code = token_store
.create_token(UserToken {
user_id: user.id,
scope: state.scope.clone(),
username: user.username.clone(),
client_id: state.client_id.clone(),
client_name: state.client_name.clone(),
Expand All @@ -240,6 +244,8 @@ 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 All @@ -258,6 +264,7 @@ pub async fn token(
form: Form<TokenFormData>,
config: &State<Config>,
token_state: &State<TokenStore<UserToken>>,
jwt_builder: &State<JWTBuilder>,
db: DbConn,
) -> Result<Json<TokenSuccess>> {
let data = form.into_inner();
Expand Down Expand Up @@ -306,13 +313,37 @@ pub async fn token(
)))
} else {
let user = User::find(token.user_id, &db).await?;
let session =
Session::create_client_session(&user, &client, &config, &db)
.await?;
let id_token = token
.scope
.as_ref()
.map(|scope| -> Option<String> {
match scope.contains("openid") {
true => {
jwt_builder.encode_id_token(&client, &user, config).ok()
},
false => None,
}
})
.flatten();

let session = Session::create_client_session(
&user,
&client,
token.scope,
&config,
&db,
)
.await?;
Ok(Json(TokenSuccess {
access_token: session.key.unwrap().clone(),
token_type: String::from("bearer"),
expires_in: config.client_session_seconds,
token_type: String::from("bearer"),
id_token,
expires_in: config.client_session_seconds,
}))
}
}

#[get("/oauth/jwks")]
pub async fn jwks(jwt_builder: &State<JWTBuilder>) -> Json<JwkSet> {
Json(jwt_builder.jwks.clone())
}
2 changes: 2 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ pub enum InternalError {
BincodeError(#[from] Box<bincode::ErrorKind>),
#[error("B64 decode error")]
Base64DecodeError(#[from] base64::DecodeError),
#[error("JWT error")]
JWTError(#[from] jsonwebtoken::errors::Error),
}
pub type InternalResult<T> = std::result::Result<T, InternalError>;

Expand Down
Loading
Loading