Dual-licensed under MIT
or the UNLICENSE.
Implements OpenID Connect Core 1.0 and OpenID Connect Discovery 1.0.
Implements UMA2 - User Managed Access, an extension to OIDC/OAuth2. Use feature flag uma2
to enable this feature.
It supports Microsoft OIDC with feature microsoft
. This adds methods for authentication and token validation, those skip issuer check.
This library is a quick and dirty rewrite of inth-oauth2 and oidc to use async / await. Basic idea was to solve particular task, as result most of good ideas from original crates were perverted and over-simplified.
Using reqwest for the HTTP client and biscuit for Javascript Object Signing and Encryption (JOSE).
Add dependency to Cargo.toml:
toml
[dependencies]
openid = "0.11"
By default it uses native tls, if you want to use rustls
:
toml
[dependencies]
openid = { version = "0.11", default-features = false, features = ["rustls"] }
This example provides only Rust part, assuming just default JHipster frontend settings.
Cargo.toml:
```toml [package] name = 'openid-example' version = '0.1.0' authors = ['Alexander Korolev alexander.korolev.germany@gmail.com'] edition = '2018'
[dependencies] anyhow = "1.0" cookie = "0.14" log = "0.4" openid = "0.11" prettyenvlogger = "0.4" reqwest = "0.11" serde = { version = "1", features = [ "derive" ] } tokio = { version = "1", features = [ "full" ] } uuid = { version = "0.8", features = [ "v4" ] } warp = "0.3" ```
src/main.rs:
```rust use std::{collections::HashMap, convert::Infallible, env, sync::Arc};
use log::{error, info}; use openid::{Client, Discovered, DiscoveredClient, Options, StandardClaims, Token, Userinfo}; use openidwarpexample::INDEX_HTML; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use warp::{ http::{Response, StatusCode}, reject, Filter, Rejection, Reply, };
type OpenIDClient = Client
const EXAMPLECOOKIE: &str = "openidwarp_example";
pub struct LoginQuery {
pub code: String,
pub state: Option
pub(crate) struct User {
pub(crate) id: String,
pub(crate) login: Option
struct Sessions {
map: HashMap async fn main() -> anyhow::Result<()> {
if env::varos("RUSTLOG").isnone() {
// Set } async fn requesttoken(
oidcclient: Arc } async fn replylogin(
oidcclient: Arc } async fn replyauthorize(oidcclient: Arc } struct Unauthorized; impl reject::Reject for Unauthorized {} async fn extractuser(
sessionid: Option fn withuser(
sessions: Arc async fn handlerejections(err: Rejection) -> Result } /// This host is the address, where user would be redirected after initial authorization.
/// For DEV environment with WebPack this is usually something like See full example: openid-examples: warp[tokio::main]
RUST_LOG=openid_warp_example=debug
to see debug logs,
// this only shows access logs.
env::setvar("RUSTLOG", "openidwarpexample=info");
}
prettyenv_logger::init();let client_id = env::var("CLIENT_ID").unwrap_or("<client id>".to_string());
let client_secret = env::var("CLIENT_SECRET").unwrap_or("<client secret>".to_string());
let issuer_url = env::var("ISSUER").unwrap_or("https://accounts.google.com".to_string());
let redirect = Some(host("/login/oauth2/code/oidc"));
let issuer = reqwest::Url::parse(&issuer_url)?;
eprintln!("redirect: {:?}", redirect);
eprintln!("issuer: {}", issuer);
let client =
Arc::new(DiscoveredClient::discover(client_id, client_secret, redirect, issuer).await?);
eprintln!("discovered config: {:?}", client.config());
let with_client = |client: Arc<Client<_>>| warp::any().map(move || client.clone());
let sessions = Arc::new(RwLock::new(Sessions::default()));
let with_sessions = |sessions: Arc<RwLock<Sessions>>| warp::any().map(move || sessions.clone());
let index = warp::path::end()
.and(warp::get())
.map(|| warp::reply::html(INDEX_HTML));
let authorize = warp::path!("oauth2" / "authorization" / "oidc")
.and(warp::get())
.and(with_client(client.clone()))
.and_then(reply_authorize);
let login = warp::path!("login" / "oauth2" / "code" / "oidc")
.and(warp::get())
.and(with_client(client.clone()))
.and(warp::query::<LoginQuery>())
.and(with_sessions(sessions.clone()))
.and_then(reply_login);
let api_account = warp::path!("api" / "account")
.and(warp::get())
.and(with_user(sessions))
.map(|user: User| warp::reply::json(&user));
let routes = index
.or(authorize)
.or(login)
.or(api_account)
.recover(handle_rejections);
let logged_routes = routes.with(warp::log("openid_warp_example"));
warp::serve(logged_routes).run(([127, 0, 0, 1], 8080)).await;
Ok(())
if let Some(mut id_token) = token.id_token.as_mut() {
oidc_client.decode_token(&mut id_token)?;
oidc_client.validate_token(&id_token, None, None)?;
info!("token: {:?}", id_token);
} else {
return Ok(None);
}
let userinfo = oidc_client.request_userinfo(&token).await?;
info!("user info: {:?}", userinfo);
Ok(Some((token, userinfo)))
let login = user_info.preferred_username.clone();
let email = user_info.email.clone();
let user = User {
id: user_info.sub.clone().unwrap_or_default(),
login,
last_name: user_info.family_name.clone(),
first_name: user_info.name.clone(),
email,
activated: user_info.email_verified,
image_url: user_info.picture.clone().map(|x| x.to_string()),
lang_key: Some("en".to_string()),
authorities: vec!["ROLE_USER".to_string()],
};
let authorization_cookie = ::cookie::Cookie::build(EXAMPLE_COOKIE, &id)
.path("/")
.http_only(true)
.finish()
.to_string();
sessions
.write()
.await
.map
.insert(id, (user, token, user_info));
let redirect_url = login_query.state.clone().unwrap_or_else(|| host("/"));
Ok(Response::builder()
.status(StatusCode::MOVED_PERMANENTLY)
.header(warp::http::header::LOCATION, redirect_url)
.header(warp::http::header::SET_COOKIE, authorization_cookie)
.body("")
.unwrap())
}
Ok(None) => {
error!("login error in call: no id_token found");
Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body("")
.unwrap())
}
Err(err) => {
error!("login error in call: {:?}", err);
Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body("")
.unwrap())
}
}
let auth_url = oidc_client.auth_url(&Options {
scope: Some("openid email profile".into()),
state: Some(origin_url),
..Default::default()
});
info!("authorize: {}", auth_url);
let url = auth_url.into_string();
Ok(warp::reply::with_header(
StatusCode::FOUND,
warp::http::header::LOCATION,
url,
))
[derive(Debug)]
Ok(warp::reply::with_status(warp::reply(), code))
http://localhost:9000
.
/// We are using http://localhost:8080
in all-in-one example.
pub fn host(path: &str) -> String {
env::var("REDIRECTURL").unwrapor("http://localhost:8080".to_string()) + path
}
```