For the past week, I have been working on JumpCloud integration for SPA frontend. I decided to write my own tutorial for my own future reference.
If you read this, I am not affiliated with JumpCloud. If you just look for the Rust part, you can skip introduction and jump to the #Integration section.
What is JumpCloud?
If you don't know JumpCloud, JumpCloud is identity management platform. They provide a lot of integrations to 3rd party apps.
Usually if you are enterprise company and looking to integrate your identity into one identity, you can integrate with JumpCloud.
For reference you can go to https://jumpcloud.com to get more detail.
JumpCloud SSO with SAML
This article will not go deeper with SSO and SAML. If you want to know the detail you can go to https://support.google.com/a/answer/6262987?hl=en. It is a great reference to read.
To start with, you need to signed up in JumpCloud and logged in as administrator. Before we create new SSO app, you can create certificate and private key from your local.
# To create cert file
$ openssl req -new -x509 -sha256 -key private.pem -out cert.pem -days 1095
# To create private key
$ openssl genrsa -out private.pem 2048
Now, you can create SSO app in JumpCloud and upload the generated certificate and private key. Then, fill ACS field to http://localhost:8000/saml/acs
. It will be your endpoint handler for SAML Response assertion.
Handling SAML Response with Rust
Here is the step by step:
- Create new rust project.
$ mkdir jumcloud-rust && cargo init
. - Add cargo dependencies
- Copy JumpCloud metadata
- Copy JumpCloud SP entity ID
- Replace below code with your JumpCloud metadata and JumpCloud SP entity ID
cargo.toml
[package]
name = "jumpcloudrust"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
openssl = "0.10"
openssl-sys = "0.9"
openssl-probe = "0.1.2"
samael="0.0.9"
tokio = { version = "1", features = ["full"] }
warp="0.3"
reqwest = { version = "0.11", features = ["json"] }
[profile.release]
lto = "fat"
codegen-units = 1
src/main.rs
use samael::metadata::{EntityDescriptor};
use samael::service_provider::ServiceProviderBuilder;
use std::collections::HashMap;
use std::fs;
use warp::{Filter};
use warp::http::{StatusCode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
openssl_probe::init_ssl_cert_env_vars();
let jc_metadata_str = "--- replace with your JC SSO App Metadata ---";
println!("{}",jc_metadata_str);
let idp_metadata: EntityDescriptor = samael::metadata::de::from_str(&jc_metadata_str)?;
let pub_key = openssl::x509::X509::from_pem(&fs::read("./cert.pem")?)?;
let private_key = openssl::rsa::Rsa::private_key_from_pem(&fs::read("./private.pem")?)?;
let sp = ServiceProviderBuilder::default()
.entity_id("--- replace with your entity id ---".to_string())
.key(private_key)
.certificate(pub_key)
.allow_idp_initiated(true)
.idp_metadata(idp_metadata)
.acs_url("http://localhost:8000/saml/acs".to_string())
.slo_url("http://localhost:8000/saml/slo".to_string())
.build()?;
let metadata = sp.metadata()?.to_xml()?;
let metadata_route = warp::get()
.and(warp::path("metadata"))
.map(move || metadata.clone());
let acs_route = warp::post()
.and(warp::path("acs"))
.and(warp::body::form())
.map(move |s: HashMap<String, String>| {
if let Some(encoded_resp) = s.get("SAMLResponse") {
println!("{:?}", encoded_resp);
let sp_res = sp.parse_response(encoded_resp, &["a_possible_request_id".to_string()]);
return match sp_res {
Ok(resp) => {
println!("{:?}", resp);
let cookie_val = format!("token={}; Path=/; Max-Age=1209600", "abc");
warp::http::Response::builder()
.header("set-cookie", string_to_static_str(cookie_val))
.header("Location", "http://localhost:3000/")
.status(StatusCode::FOUND)
.body("".to_string())
},
Err(e) => warp::http::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(e.to_string())
}
}
return warp::http::Response::builder()
.status(StatusCode::FORBIDDEN)
.body("Error FORBIDDEN".to_string())
});
let saml_routes = warp::path("saml").and(acs_route.or(metadata_route));
warp::serve(saml_routes).run(([127, 0, 0, 1], 8000)).await;
Ok(())
}
fn string_to_static_str(s: String) -> &'static str {
Box::leak(s.into_boxed_str())
}
Now, run the app. Go to terminal and type cargo run --release
. That's the BE part. See my (github)[github.com/rhzs/rust-saml-jumpcloud-sso] for full Rust implementation.
Preparing JumpCloud for SPA Frontend
The frontend part is a straight forward implementation. Go to your terminal and clone my github repo.
This will contain the following implementation and logic:
- A login page with JumpCloud login redirection button
- A home page and its welcome message after you logged in
- A redirection to login page, when you try to click home page when not logged in.
- A logout button to clear credentials
Putting it together
To run it together, do the following:
- Open 2 different terminals, one for backend and one for frontend.
- Try to open frontend and click login with JumpCloud
- You will be redirected to JumpCloud login page.
- After logged in, you should be redirected to your application.
- JumpCloud will response to your Backend by calling API from the input ACS handler field. So, if you put
http://localhost:8000/saml/acs
- JumpCloud will perform POST with encoded form operation to the designated API endpoint. In backend, backend will accept this request, perform SAML assertion, and then instruct redirection to frontend.
Violaa!! It's a success! Congratulations! You are able to integrate JumpCloud SSO with SPA frontend and your own Rust backend.
This tutorial is confirmed 100% working as of 3 July 2022.
Please forgive me, if the tutorial is somehow lacking information. This is not intended to be detailed tutorial, but rather author own documentation.
Top comments (0)