OpenID Connect & Discovery client library using async / await

Legal

Dual-licensed under MIT or the UNLICENSE.

Features

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).

Usage

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"] }

Use case: Warp web server with JHipster generated frontend and Google OpenID Connect

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";

[derive(Deserialize, Debug)]

pub struct LoginQuery { pub code: String, pub state: Option, }

[derive(Serialize, Deserialize, Debug, Default, Clone)]

[serde(rename_all = "camelCase")]

pub(crate) struct User { pub(crate) id: String, pub(crate) login: Option, pub(crate) firstname: Option, pub(crate) lastname: Option, pub(crate) email: Option, pub(crate) imageurl: Option, pub(crate) activated: bool, pub(crate) langkey: Option, pub(crate) authorities: Vec, }

[derive(Default)]

struct Sessions { map: HashMap

[tokio::main]

async fn main() -> anyhow::Result<()> { if env::varos("RUSTLOG").isnone() { // Set 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(())

}

async fn requesttoken( oidcclient: Arc, loginquery: &LoginQuery, ) -> anyhow::Resultclient.requesttoken(&loginquery.code).await?.into();

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)))

}

async fn replylogin( oidcclient: Arc, loginquery: LoginQuery, sessions: Arc>, ) -> Result { let requesttoken = requesttoken(oidcclient, &loginquery).await; match requesttoken { Ok(Some((token, userinfo))) => { let id = uuid::Uuid::newv4().to_string();

        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())
    }
}

}

async fn replyauthorize(oidcclient: Arc) -> Result { let originurl = env::var("ORIGIN").unwrapor(host(""));

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)]

struct Unauthorized;

impl reject::Reject for Unauthorized {}

async fn extractuser( sessionid: Option, sessions: Arc>, ) -> Result { if let Some(sessionid) = sessionid { if let Some((user, , _)) = sessions.read().await.map.get(&sessionid) { Ok(user.clone()) } else { Err(warp::reject::custom(Unauthorized)) } } else { Err(warp::reject::custom(Unauthorized)) } }

fn withuser( sessions: Arc>, ) -> impl Filter + Clone { warp::cookie::optional(EXAMPLECOOKIE) .and(warp::any().map(move || sessions.clone())) .andthen(extractuser) }

async fn handlerejections(err: Rejection) -> Result { let code = if err.isnotfound() { StatusCode::NOTFOUND } else if let Some(Unauthorized) = err.find() { StatusCode::UNAUTHORIZED } else { StatusCode::INTERNALSERVERERROR };

Ok(warp::reply::with_status(warp::reply(), code))

}

/// This host is the address, where user would be redirected after initial authorization. /// For DEV environment with WebPack this is usually something like 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 } ```

See full example: openid-examples: warp