oauth 0.0.2

Universal OAuth 2.0 adapter for Rust web frameworks
Documentation
use std::sync::Arc;
use std::time::Duration;

use crate::error::{ConfigError, OAuthError};
use crate::token_store::{InMemoryTokenStore, TokenStore};

/// Immutable OAuth configuration shared across framework adapters.
#[derive(Clone)]
pub struct OAuthConfig {
    client_id: String,
    client_secret: String,
    issuer: Option<String>,
    access_token_ttl: Duration,
    refresh_token_ttl: Option<Duration>,
    refresh_tokens_enabled: bool,
    token_store: Arc<dyn TokenStore>,
}

impl OAuthConfig {
    /// Start building a new configuration.
    pub fn builder() -> OAuthConfigBuilder {
        OAuthConfigBuilder::default()
    }

    /// Client identifier used for confidential OAuth flows.
    pub fn client_id(&self) -> &str {
        &self.client_id
    }

    /// Token issuer URL, if defined.
    pub fn issuer(&self) -> Option<&str> {
        self.issuer.as_deref()
    }

    /// Access token time-to-live.
    pub fn access_token_ttl(&self) -> Duration {
        self.access_token_ttl
    }

    /// Refresh token time-to-live, when enabled.
    pub fn refresh_token_ttl(&self) -> Option<Duration> {
        self.refresh_token_ttl
    }

    /// Whether refresh tokens are issued.
    pub fn refresh_tokens_enabled(&self) -> bool {
        self.refresh_tokens_enabled
    }

    /// The configured token store.
    pub fn token_store(&self) -> Arc<dyn TokenStore> {
        Arc::clone(&self.token_store)
    }

    pub(crate) fn verify_client(
        &self,
        client_id: &str,
        client_secret: &str,
    ) -> Result<(), OAuthError> {
        if client_id != self.client_id || client_secret != self.client_secret {
            return Err(OAuthError::InvalidClient);
        }
        Ok(())
    }
}

/// Builder for [`OAuthConfig`].
#[derive(Default)]
pub struct OAuthConfigBuilder {
    client_id: Option<String>,
    client_secret: Option<String>,
    issuer: Option<String>,
    access_token_ttl: Option<Duration>,
    refresh_token_ttl: Option<Duration>,
    refresh_tokens_enabled: bool,
    token_store: Option<Arc<dyn TokenStore>>,
}

impl OAuthConfigBuilder {
    /// Set the OAuth client identifier.
    pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
        self.client_id = Some(client_id.into());
        self
    }

    /// Set the OAuth client secret.
    pub fn client_secret(mut self, client_secret: impl Into<String>) -> Self {
        self.client_secret = Some(client_secret.into());
        self
    }

    /// Set the issuer (iss) claim value.
    pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
        self.issuer = Some(issuer.into());
        self
    }

    /// Set access token time-to-live.
    pub fn access_token_ttl(mut self, ttl: Duration) -> Self {
        self.access_token_ttl = Some(ttl);
        self
    }

    /// Set refresh token time-to-live.
    pub fn refresh_token_ttl(mut self, ttl: Duration) -> Self {
        self.refresh_token_ttl = Some(ttl);
        self
    }

    /// Enable or disable refresh token issuance.
    pub fn enable_refresh_tokens(mut self, enabled: bool) -> Self {
        self.refresh_tokens_enabled = enabled;
        self
    }

    /// Override the backing token store.
    pub fn token_store<T>(mut self, store: T) -> Self
    where
        T: TokenStore + 'static,
    {
        self.token_store = Some(Arc::new(store));
        self
    }

    /// Build the [`OAuthConfig`], validating required fields.
    pub fn build(self) -> Result<OAuthConfig, ConfigError> {
        let client_id = self
            .client_id
            .ok_or(ConfigError::MissingField("client_id"))?;
        let client_secret = self
            .client_secret
            .ok_or(ConfigError::MissingField("client_secret"))?;
        let access_token_ttl = self
            .access_token_ttl
            .unwrap_or_else(|| Duration::from_secs(3600));

        if self.refresh_tokens_enabled && self.refresh_token_ttl.is_none() {
            return Err(ConfigError::InvalidValue {
                field: "refresh_token_ttl",
                message: "must be set when refresh tokens are enabled".into(),
            });
        }

        let token_store = self
            .token_store
            .unwrap_or_else(|| Arc::new(InMemoryTokenStore::default()));

        Ok(OAuthConfig {
            client_id,
            client_secret,
            issuer: self.issuer,
            access_token_ttl,
            refresh_token_ttl: self.refresh_token_ttl,
            refresh_tokens_enabled: self.refresh_tokens_enabled,
            token_store,
        })
    }
}