socio_providers 0.1.1

Social login providers for socio
Documentation
use serde::{Deserialize, Serialize};
use socio::{
    Socio, async_trait, error,
    jwt::verify_jwt_with_jwks_endpoint,
    oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, Scope, TokenUrl},
    providers::{SocioProvider, StandardUser, UserAwareSocioProvider},
    types::{OpenIdTokenField, Response, SocioClient},
};
use url_macro::url;

#[derive(Clone, Debug)]
pub struct Google;

#[derive(Debug, Serialize, Deserialize)]
pub struct GoogleUser {
    iss: String,
    aud: String,
    sub: String,
    email: Option<String>,
    email_verified: Option<bool>,
    name: Option<String>,
    picture: Option<String>,
}

#[async_trait]
impl SocioProvider for Google {
    async fn exchange_code_standard(
        &self,
        client: &SocioClient,
        code: socio::oauth2::AuthorizationCode,
        pkce_verifier: socio::oauth2::PkceCodeVerifier,
    ) -> error::Result<Response<StandardUser>> {
        Ok(self
            .exchange_code_for_user(client, code, pkce_verifier)
            .await?
            .standardize())
    }
}

#[async_trait]
impl UserAwareSocioProvider for Google {
    type User = GoogleUser;

    async fn exchange_code_for_user(
        &self,
        client: &SocioClient,
        code: socio::oauth2::AuthorizationCode,
        pkce_verifier: socio::oauth2::PkceCodeVerifier,
    ) -> error::Result<Response<Self::User>> {
        let response = client
            .exchange_code::<OpenIdTokenField>(code, pkce_verifier)
            .await?;

        let token = verify_jwt_with_jwks_endpoint::<GoogleUser>(
            &response.extra_fields().id_token,
            "https://www.googleapis.com/oauth2/v3/certs",
            &client.client_id,
        )
        .await?;

        Ok(Response::from_standard_token_response(
            &response,
            token.claims,
        ))
    }
}

impl From<GoogleUser> for StandardUser {
    fn from(value: GoogleUser) -> Self {
        StandardUser {
            id: value.sub,
            name: value.name,
            email: value.email,
            picture: value.picture,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GoogleConfig {
    pub client_id: ClientId,
    pub client_secret: ClientSecret,
    pub redirect_url: RedirectUrl,
}

impl From<GoogleConfig> for SocioClient {
    fn from(value: GoogleConfig) -> Self {
        let auth_url = url!("https://accounts.google.com/o/oauth2/auth");
        let token_url = url!("https://accounts.google.com/o/oauth2/token");

        SocioClient {
            client_id: value.client_id,
            client_secret: value.client_secret,
            redirect_uri: value.redirect_url,
            authorize_endpoint: AuthUrl::from_url(auth_url), // SAFETY: This is safe because the URL is valid
            token_endpoint: TokenUrl::from_url(token_url), // SAFETY: This is safe because the URL is valid
            scopes: ["openid", "profile", "email"]
                .iter()
                .map(|s| Scope::new(s.to_string()))
                .collect(),
        }
    }
}

impl From<GoogleConfig> for Socio<Google> {
    fn from(value: GoogleConfig) -> Self {
        Socio::new(value.into(), Google)
    }
}