auth: Implement OpenID Connect login flow
This commit adds two path operations, *GET /login* and *GET /oidc-callback*, which initiate and complete the OpenID connect login flow, respectively. Only the *Authorization Code* flow is supported, since this is the only flow implemented by Authelia. There is quite a bit of boilerplate required to fully implement an OIDC relying party, especially in Rust. The documentation for `openidconnect` is decent, but it still took quite a bit of trial and error to get everything working. After successfully finishing the OIDC login, the client will receive a cookie containing a JWT that can be used for further communication with the server. We're not using the OIDC tokens themselves for authorization. For development and testing, Dex is a simple and convenient OIDC IdP. The only caveat is its configuration file must contain list the TCP port clients will use to connect to it, meaning we cannot use Podman dynamic port allocation like we do for Meilisearch. Ultimately, this just means the integration tests will fail if there is another process already listening on 5556.
This commit is contained in:
84
tests/integration/auth.rs
Normal file
84
tests/integration/auth.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use rocket::http::Status;
|
||||
use rocket::local::asynchronous::Client;
|
||||
use rocket::uri;
|
||||
use scraper::{Html, Selector};
|
||||
use tracing::debug;
|
||||
|
||||
use seensite::auth::*;
|
||||
use seensite::Context;
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn test_login() {
|
||||
super::setup();
|
||||
|
||||
let client = Client::tracked(seensite::rocket()).await.unwrap();
|
||||
let dex = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.cookie_store(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let ctx: &Context = client.rocket().state().unwrap();
|
||||
|
||||
// First, initiate the login process
|
||||
let req = client.get(uri![oidc_login]);
|
||||
let res = req.dispatch().await;
|
||||
assert_eq!(res.status(), Status::SeeOther);
|
||||
let mut location = res.headers().get_one("Location").unwrap().to_string();
|
||||
|
||||
// Next, follow the redirect URL provided by the login page.
|
||||
// This will redirect again.
|
||||
let res = loop {
|
||||
debug!("Redirect: {}", location);
|
||||
let res = dex.get(location).send().await.unwrap();
|
||||
if res.status() == reqwest::StatusCode::FOUND {
|
||||
let base_url = res.url().clone();
|
||||
location = base_url
|
||||
.join(res.headers().get("Location").unwrap().to_str().unwrap())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
continue;
|
||||
}
|
||||
break res;
|
||||
};
|
||||
|
||||
// After all the redirects, we end up on the IdP login form.
|
||||
assert_eq!(res.status(), reqwest::StatusCode::OK);
|
||||
// Obtain the login form target
|
||||
let base_url = res.url().clone();
|
||||
let body = res.text().await;
|
||||
let doc = Html::parse_fragment(&body.unwrap());
|
||||
let sel = Selector::parse("form").unwrap();
|
||||
let form = doc.select(&sel).next().unwrap();
|
||||
let action = form.attr("action").unwrap();
|
||||
let url = base_url.join(action).unwrap();
|
||||
// Post the user credentials to the IdP login form
|
||||
let res = dex
|
||||
.post(url)
|
||||
.form(&[("login", "user@example.com"), ("password", "password")])
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(res.status(), reqwest::StatusCode::SEE_OTHER);
|
||||
|
||||
// The result of the IdP login form submission is another redirect
|
||||
// to the OIDC callback of our application.
|
||||
let location = reqwest::Url::parse(
|
||||
res.headers().get("Location").unwrap().to_str().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let callback =
|
||||
format!("{}?{}", location.path(), location.query().unwrap());
|
||||
debug!("Callback: {}", callback);
|
||||
|
||||
// Finally, make the callback request to finish the login process.
|
||||
let res = client.get(callback).dispatch().await;
|
||||
assert_eq!(res.status(), Status::SeeOther);
|
||||
let location = res.headers().get_one("Location").unwrap().to_string();
|
||||
assert_eq!(location, "/");
|
||||
let cookie = res.cookies().get("auth.token").unwrap();
|
||||
debug!("Cookie: {:?}", cookie);
|
||||
let claims = ctx.decode_jwt(cookie.value()).unwrap();
|
||||
debug!("Claims: {:?}", claims);
|
||||
assert!(!claims.sub.is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user