#oauth #authorization #auth-token #http-client #oauth2 #uma2

openid

使用 async / await 的 OpenID Connect & Discovery 客户端库

25 个版本 (13 个重大更改)

0.14.0 2024年4月21日
0.12.1 2023年9月17日
0.12.0 2023年3月30日
0.11.0 2022年11月28日
0.2.1 2020年3月1日

#17 in 身份验证

Download history 19561/week @ 2024-05-04 24562/week @ 2024-05-11 27448/week @ 2024-05-18 27008/week @ 2024-05-25 29709/week @ 2024-06-01 21237/week @ 2024-06-08 20831/week @ 2024-06-15 29139/week @ 2024-06-22 20354/week @ 2024-06-29 19534/week @ 2024-07-06 27215/week @ 2024-07-13 29519/week @ 2024-07-20 23028/week @ 2024-07-27 21364/week @ 2024-08-03 14978/week @ 2024-08-10 14639/week @ 2024-08-17

79,282 每月下载量
13 个Crates中使用 (7 个直接使用)

Unlicense OR MIT

140KB
2.5K SLoC

使用 async / await 的 OpenID Connect & Discovery 客户端库

双许可下 MITUNLICENSE.

功能

实现 OpenID Connect Core 1.0OpenID Connect Discovery 1.0.

实现 UMA2 - 用户管理访问,OIDC/OAuth2的扩展。使用功能标志 uma2 启用此功能。

它支持具有功能 microsoft 的 Microsoft OIDC。这添加了身份验证和令牌验证方法,这些方法跳过发行者检查。

最初作为对异步/等待功能快速适应的快速版本开发,基于 inth-oauth2oidc,该库随后发展成为成熟且健壮的解决方案,提供扩展功能和改进的性能。

使用 reqwest 作为 HTTP 客户端和 biscuit 用于 JavaScript 对象签名和加密 (JOSE)。

支持

您可以通过各种方式为 OpenID 库的持续开发和维护做出贡献

赞助

无论您的支持大小如何,都有助于维持项目并确保其持续改进。请与我们联系,了解赞助机会。

反馈

无论您是开发者、用户还是爱好者,您的反馈都非常有价值。分享您的想法、建议和想法,以帮助塑造库的未来。

贡献

如果您对开源充满热情并且有可以分享的技能,请考虑为项目做出贡献。每一项贡献都很重要!

感谢您成为 OpenID 社区的一部分。我们一起让身份验证过程对每个人来说都更容易、更可靠和更高效。

使用

将依赖项添加到 Cargo.toml 中

[dependencies]
openid = "0.14"

默认情况下我们使用本机 tls,如果您想使用 rustls

[dependencies]
openid = { version = "0.14", default-features = false, features = ["rustls"] }

用例:使用 Warp 网络服务器和 JHipster 生成的前端以及 Google OpenID Connect

此示例仅提供 Rust 部分,假设仅为默认的 JHipster 前端设置。

在 Cargo.toml 中

[dependencies]
anyhow = "1.0"
cookie = "0.18"
dotenv = "0.15"
log = "0.4"
openid = "0.14"
pretty_env_logger = "0.5"
reqwest = "0.12"
serde = { version = "1", default-features = false, features = [ "derive" ] }
serde_json = "1"
tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "macros" ] }
uuid = { version = "1.0", default-features = false, features = [ "v4" ] }
warp = { version = "0.3", default-features = false }

[patch.crates-io]
openid = { path = "../../openid" }

在 src/main.rs 中

use std::{convert::Infallible, env, net::SocketAddr, sync::Arc};

use cookie::time::Duration;
use log::{error, info};
use openid::{Client, Discovered, DiscoveredClient, Options, StandardClaims, Token, Userinfo};
use openid_examples::{
    entity::{LoginQuery, Sessions, User},
    INDEX_HTML,
};
use tokio::sync::RwLock;
use warp::{
    http::{Response, StatusCode},
    reject, Filter, Rejection, Reply,
};

type OpenIDClient = Client<Discovered, StandardClaims>;

const EXAMPLE_COOKIE: &str = "openid_warp_example";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenv::dotenv().ok();

    pretty_env_logger::init();

    let client_id = env::var("CLIENT_ID").expect("<client id> for your provider");
    let client_secret = env::var("CLIENT_SECRET").ok();
    let issuer_url =
        env::var("ISSUER").unwrap_or_else(|_| "http://127.0.0.1".to_string());
    let redirect = Some(host("/login/oauth2/code/oidc"));
    let issuer = reqwest::Url::parse(&issuer_url)?;
    let listen: SocketAddr = env::var("LISTEN")
        .unwrap_or_else(|_| "127.0.0.1:8080".to_string())
        .parse()?;

    info!("redirect: {:?}", redirect);
    info!("issuer: {}", issuer);

    let client = Arc::new(
        DiscoveredClient::discover(
            client_id,
            client_secret.unwrap_or_default(),
            redirect,
            issuer,
        )
        .await?,
    );

    info!("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 logout = warp::path!("logout")
        .and(warp::get())
        .and(with_client(client.clone()))
        .and(warp::cookie::optional(EXAMPLE_COOKIE))
        .and(with_sessions(sessions.clone()))
        .and_then(reply_logout);

    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(logout)
        .or(api_account)
        .recover(handle_rejections);

    let logged_routes = routes.with(warp::log("openid_warp_example"));

    warp::serve(logged_routes).run(listen).await;

    Ok(())
}

async fn request_token(
    oidc_client: &OpenIDClient,
    login_query: &LoginQuery,
) -> anyhow::Result<Option<(Token, Userinfo)>> {
    let mut token: Token = oidc_client.request_token(&login_query.code).await?.into();

    if let Some(id_token) = token.id_token.as_mut() {
        oidc_client.decode_token(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 reply_login(
    oidc_client: Arc<OpenIDClient>,
    login_query: LoginQuery,
    sessions: Arc<RwLock<Sessions>>,
) -> Result<impl warp::Reply, Infallible> {
    let request_token = request_token(&oidc_client, &login_query).await;
    match request_token {
        Ok(Some((token, user_info))) => {
            let id = uuid::Uuid::new_v4().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)
                .build()
                .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");

            response_unauthorized()
        }
        Err(err) => {
            error!("login error in call: {:?}", err);

            response_unauthorized()
        }
    }
}

fn response_unauthorized() -> Result<Response<&'static str>, Infallible> {
    Ok(Response::builder()
        .status(StatusCode::UNAUTHORIZED)
        .body("")
        .unwrap())
}

async fn reply_logout(
    oidc_client: Arc<OpenIDClient>,
    session_id: Option<String>,
    sessions: Arc<RwLock<Sessions>>,
) -> Result<impl warp::Reply, Infallible> {
    let Some(id) = session_id else {
        return response_unauthorized();
    };

    let session_removed = sessions.write().await.map.remove(&id);

    if let Some(id_token) = session_removed.and_then(|(_, token, _)| token.bearer.id_token) {
        let authorization_cookie = ::cookie::Cookie::build((EXAMPLE_COOKIE, &id))
            .path("/")
            .http_only(true)
            .max_age(Duration::seconds(-1))
            .build()
            .to_string();

        let return_redirect_url = host("/");

        let redirect_url = oidc_client
            .config()
            .end_session_endpoint
            .clone()
            .map(|mut logout_provider_endpoint| {
                logout_provider_endpoint
                    .query_pairs_mut()
                    .append_pair("id_token_hint", &id_token)
                    .append_pair("post_logout_redirect_uri", &return_redirect_url);
                logout_provider_endpoint.to_string()
            })
            .unwrap_or_else(|| return_redirect_url);

        info!("logout redirect url: {redirect_url}");

        Ok(Response::builder()
            .status(StatusCode::FOUND)
            .header(warp::http::header::LOCATION, redirect_url)
            .header(warp::http::header::SET_COOKIE, authorization_cookie)
            .body("")
            .unwrap())
    } else {
        response_unauthorized()
    }
}

async fn reply_authorize(oidc_client: Arc<OpenIDClient>) -> Result<impl warp::Reply, Infallible> {
    let origin_url = env::var("ORIGIN").unwrap_or_else(|_| 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: String = auth_url.into();

    Ok(warp::reply::with_header(
        StatusCode::FOUND,
        warp::http::header::LOCATION,
        url,
    ))
}

#[derive(Debug)]
struct Unauthorized;

impl reject::Reject for Unauthorized {}

async fn extract_user(
    session_id: Option<String>,
    sessions: Arc<RwLock<Sessions>>,
) -> Result<User, Rejection> {
    if let Some(session_id) = session_id {
        if let Some((user, _, _)) = sessions.read().await.map.get(&session_id) {
            Ok(user.clone())
        } else {
            Err(warp::reject::custom(Unauthorized))
        }
    } else {
        Err(warp::reject::custom(Unauthorized))
    }
}

fn with_user(
    sessions: Arc<RwLock<Sessions>>,
) -> impl Filter<Extract = (User,), Error = Rejection> + Clone {
    warp::cookie::optional(EXAMPLE_COOKIE)
        .and(warp::any().map(move || sessions.clone()))
        .and_then(extract_user)
}

async fn handle_rejections(err: Rejection) -> Result<impl Reply, Infallible> {
    let code = if err.is_not_found() {
        StatusCode::NOT_FOUND
    } else if let Some(Unauthorized) = err.find() {
        StatusCode::UNAUTHORIZED
    } else {
        StatusCode::INTERNAL_SERVER_ERROR
    };

    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://127.0.0.1:9000`.
/// We are using `http://127.0.0.1:8080` in all-in-one example.
pub fn host(path: &str) -> String {
    env::var("REDIRECT_URL").unwrap_or_else(|_| "http://127.0.0.1:8080".to_string()) + path
}

查看完整示例: openid-examples: warp

依赖项

~15–28MB
~525K SLoC