better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use josekit::{jwk::Jwk, jws::JwsHeader};
use serde_json::{Value, json};
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

use super::OAuthProvider;

pub(super) struct GoogleFixture {
    pub(super) url: String,
    pub(super) token: String,
    pub(super) requests: Arc<Mutex<Vec<String>>>,
    task: tokio::task::JoinHandle<()>,
}

impl GoogleFixture {
    pub(super) async fn start(claims: Value) -> Self {
        Self::start_with_userinfo(claims, None).await
    }

    pub(super) async fn start_with_userinfo(mut claims: Value, userinfo: Option<Value>) -> Self {
        let mut key = Jwk::generate_rsa_key(2048).unwrap();
        key.set_key_id("google-normal-fixture");
        let mut public = key.to_public_key().unwrap();
        public.set_key_id("google-normal-fixture");
        let now = chrono::Utc::now().timestamp();
        let fields = claims.as_object_mut().unwrap();
        for (name, value) in [
            ("iss", json!("https://accounts.google.com")),
            ("aud", json!("client")),
            ("iat", json!(now)),
            ("exp", json!(now + 3600)),
        ] {
            let _ = fields.entry(name).or_insert(value);
        }
        let mut header = JwsHeader::new();
        header.set_algorithm("RS256");
        header.set_key_id("google-normal-fixture");
        let token = josekit::jws::serialize_compact(
            claims.to_string().as_bytes(),
            &header,
            &josekit::jws::RS256.signer_from_jwk(&key).unwrap(),
        )
        .unwrap();
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let url = format!("http://{}", listener.local_addr().unwrap());
        let requests = Arc::new(Mutex::new(Vec::new()));
        let logged = requests.clone();
        let signed_token = token.clone();
        let task = tokio::spawn(async move {
            loop {
                let (mut stream, _) = listener.accept().await.unwrap();
                let mut bytes = [0; 8192];
                let size = stream.read(&mut bytes).await.unwrap();
                let request = String::from_utf8_lossy(&bytes[..size]);
                let path = request.split_whitespace().nth(1).unwrap();
                logged.lock().unwrap().push(path.to_owned());
                let body = match path {
                    "/jwks" => json!({"keys":[public]}),
                    "/token" => json!({"id_token":signed_token,"access_token":"normal-access","expires_in":3600}),
                    "/userinfo" if userinfo.is_some() => userinfo.clone().unwrap(),
                    _ => panic!("unexpected Google fixture request: {path}"),
                }
                .to_string();
                let response = format!(
                    "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
                    body.len()
                );
                stream.write_all(response.as_bytes()).await.unwrap();
            }
        });
        Self {
            url,
            token,
            requests,
            task,
        }
    }

    pub(super) fn provider(&self) -> OAuthProvider {
        let mut provider = OAuthProvider::google("client", "secret");
        self.configure(&mut provider);
        provider
    }

    pub(super) fn configure(&self, provider: &mut OAuthProvider) {
        provider.set_google_jwks_url(format!("{}/jwks", self.url));
        provider.token_url = format!("{}/token", self.url);
        provider.user_info_url = Some(format!("{}/userinfo", self.url));
    }
}

impl Drop for GoogleFixture {
    fn drop(&mut self) {
        self.task.abort();
    }
}