light-openid 1.1.1

Lightweight OpenID primitives & client
Documentation
//! # Open ID client implementation

use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use std::collections::HashMap;

use crate::primitives::{OpenIDConfig, OpenIDTokenResponse, OpenIDUserInfo};
use crate::Res;

impl OpenIDConfig {
    /// Load OpenID configuration from a given .well-known/openid-configuration URL
    #[tracing::instrument]
    pub async fn load_from_url(url: &str) -> Res<Self> {
        Ok(reqwest::get(url).await?.json().await?)
    }

    /// Get the authorization URL where a user should be redirect to perform authentication
    #[tracing::instrument(skip(self, client_id, state, redirect_uri))]
    pub fn gen_authorization_url(
        &self,
        client_id: &str,
        state: &str,
        redirect_uri: &str,
    ) -> String {
        let client_id = urlencoding::encode(client_id);
        let state = urlencoding::encode(state);
        let redirect_uri = urlencoding::encode(redirect_uri);

        format!("{}?response_type=code&scope=openid%20profile%20email&client_id={client_id}&state={state}&redirect_uri={redirect_uri}", self.authorization_endpoint)
    }

    /// Query the token endpoint
    ///
    /// This endpoint returns both the parsed and the raw response, to allow handling
    /// of bonus fields
    #[tracing::instrument(skip(self, client_id, client_secret, code, redirect_uri))]
    pub async fn request_token(
        &self,
        client_id: &str,
        client_secret: &str,
        code: &str,
        redirect_uri: &str,
    ) -> Res<(OpenIDTokenResponse, String)> {
        let authorization = BASE64_STANDARD.encode(format!("{client_id}:{client_secret}"));

        let mut params = HashMap::new();
        params.insert("grant_type", "authorization_code");
        params.insert("code", code);
        params.insert("redirect_uri", redirect_uri);

        let response = reqwest::Client::new()
            .post(&self.token_endpoint)
            .header("Authorization", format!("Basic {authorization}"))
            .form(&params)
            .send()
            .await?
            .text()
            .await?;

        Ok((serde_json::from_str(&response)?, response))
    }

    /// Query the UserInfo endpoint.
    ///
    /// This endpoint should be used after having successfully retrieved the token
    ///
    /// This endpoint returns both the parsed value and the raw response, in case of presence
    /// of additional fields
    #[tracing::instrument(skip(self, token))]
    pub async fn request_user_info(
        &self,
        token: &OpenIDTokenResponse,
    ) -> Res<(OpenIDUserInfo, String)> {
        let response = reqwest::Client::new()
            .get(self.userinfo_endpoint.as_ref().expect(
                "This client only support information retrieval through userinfo endpoint!",
            ))
            .header("Authorization", format!("Bearer {}", token.access_token))
            .send()
            .await?
            .text()
            .await?;

        Ok((serde_json::from_str(&response)?, response))
    }
}