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.

This is 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.3"

Use case: Actix 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 kilork@yandex.ru'] edition = '2018'

[dependencies] actix = '0.9' actix-identity = '0.2' actix-rt = '1.0' exitfailure = "0.5" uuid = { version = "0.8", features = [ "v4" ] } url = "2.1" openid = "0.3"

[dependencies.serde] version = '1.0' features = ['derive']

[dependencies.reqwest] version = '0.10' features = ['json']

[dependencies.actix-web] version = '2.0' features = ['rustls'] ```

src/main.rs:

```rust

[macro_use]

extern crate actix_web;

use actix::prelude::*; use actixidentity::{CookieIdentityPolicy, Identity, IdentityService}; use actixweb::{ dev::Payload, error::ErrorUnauthorized, http, middleware, web, App, Error, FromRequest, HttpRequest, HttpResponse, HttpServer, Responder, }; use exitfailure::ExitFailure; use openid::{DiscoveredClient, Options, Token, Userinfo}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, pin::Pin, sync::RwLock}; use url::Url;

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

[serde(rename_all = "camelCase")]

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

[derive(Serialize, Deserialize, Debug)]

[serde(rename_all = "camelCase")]

struct Logout { idtoken: String, logouturl: Option, }

impl FromRequest for User { type Config = (); type Error = Error; type Future = Pin>>>;

fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
    let fut = Identity::from_request(req, pl);
    let sessions: Option<&web::Data<RwLock<Sessions>>> = req.app_data();
    if sessions.is_none() {
        eprintln!("sessions is none!");
        return Box::pin(async { Err(ErrorUnauthorized("unauthorized")) });
    }
    let sessions = sessions.unwrap().clone();

    Box::pin(async move {
        if let Some(identity) = fut.await?.identity() {
            if let Some(user) = sessions
                .read()
                .unwrap()
                .map
                .get(&identity)
                .map(|x| x.0.clone())
            {
                return Ok(user);
            }
        };

        Err(ErrorUnauthorized("unauthorized"))
    })
}

}

struct Sessions { map: HashMap

[derive(Serialize, Deserialize, Debug)]

struct Failure { error: String, }

[get("/oauth2/authorization/oidc")]

async fn authorize(oidcclient: web::Data) -> impl Responder { let authurl = oidcclient.authurl(&Options { scope: Some("email".into()), ..Default::default() });

eprintln!("authorize: {}", auth_url);

HttpResponse::Found()
    .header(http::header::LOCATION, auth_url.to_string())
    .finish()

}

[get("/account")]

async fn account(user: User) -> impl Responder { web::Json(user) }

[derive(Deserialize, Debug)]

struct LoginQuery { code: String, }

async fn requesttoken( oidcclient: web::Data, query: web::Query, ) -> Result { let mut token: Token = oidcclient.requesttoken(&query.code).await?.into(); if let Some(mut idtoken) = token.idtoken.asmut() { oidcclient.decodetoken(&mut idtoken)?; oidcclient.validatetoken(&idtoken, None, None)?; eprintln!("token: {:?}", idtoken); } else { return Ok(None); } let userinfo = oidcclient.requestuserinfo(&token).await?;

eprintln!("user info: {:?}", userinfo);
Ok(Some((token, userinfo)))

}

[get("/login/oauth2/code/oidc")]

async fn login( oidc_client: web::Data, query: web::Query, sessions: web::Data>, identity: Identity, ) -> impl Responder { eprintln!("login: {:?}", query);

match request_token(oidc_client, query).await {
    Ok(Some((token, userinfo))) => {
        let id = uuid::Uuid::new_v4().to_string();

        let login = userinfo.preferred_username.clone();
        let email = userinfo.email.clone();

        let user = User {
            id: userinfo.sub.clone().unwrap_or_default(),
            login,
            last_name: userinfo.family_name.clone(),
            first_name: userinfo.name.clone(),
            email,
            activated: userinfo.email_verified,
            image_url: userinfo.picture.clone().map(|x| x.to_string()),
            lang_key: Some("en".to_string()),
            authorities: vec!["ROLE_USER".to_string()], //FIXME: read from token
        };

        identity.remember(id.clone());
        sessions
            .write()
            .unwrap()
            .map
            .insert(id, (user, token, userinfo));

        HttpResponse::Found()
            .header(http::header::LOCATION, host("/"))
            .finish()
    }
    Ok(None) => {
        eprintln!("login error in call: no id_token found");

        HttpResponse::Unauthorized().finish()
    }
    Err(err) => {
        eprintln!("login error in call: {:?}", err);

        HttpResponse::Unauthorized().finish()
    }
}

}

[post("/logout")]

async fn logout( oidc_client: web::Data, sessions: web::Data>, identity: Identity, ) -> impl Responder { if let Some(id) = identity.identity() { identity.forget(); if let Some((user, token, _userinfo)) = sessions.write().unwrap().map.remove(&id) { eprintln!("logout user: {:?}", user);

        let id_token = token.bearer.access_token.into();
        let logout_url = oidc_client.config().end_session_endpoint.clone();

        return HttpResponse::Ok().json(Logout {
            id_token,
            logout_url,
        });
    }
}

HttpResponse::Unauthorized().finish()

}

fn host(path: &str) -> String { "http://localhost:9000".to_string() + path }

[actix_rt::main]

async fn main() -> Result<(), ExitFailure> { let clientid = "".tostring(); let clientsecret = "".tostring(); let redirect = Some(host("/login/oauth2/code/oidc")); let issuer = reqwest::Url::parse("https://accounts.google.com")?; eprintln!("redirect: {:?}", redirect); eprintln!("issuer: {}", issuer); let client = openid::DiscoveredClient::discover(clientid, clientsecret, redirect, issuer).await?;

eprintln!("discovered config: {:?}", client.config());

let client = web::Data::new(client);

let sessions = web::Data::new(RwLock::new(Sessions {
    map: HashMap::new(),
}));

HttpServer::new(move || {
    App::new()
        .wrap(middleware::Logger::default())
        .wrap(IdentityService::new(
            CookieIdentityPolicy::new(&[0; 32])
                .name("auth-openid")
                .secure(false),
        ))
        .app_data(client.clone())
        .app_data(sessions.clone())
        .service(authorize)
        .service(login)
        .service(web::scope("/api").service(account).service(logout))
})
.bind("localhost:8080")?
.run()
.await?;

Ok(())

} ```