Skip to main content

ez_token/services/authentication/
client_credentials.rs

1use crate::cli::output::{finish_spinner_error, finish_spinner_success, start_spinner};
2use crate::services::authentication::authenticator::Authenticator;
3use crate::services::authentication::urls::IdentityProvider;
4use crate::services::http_client::client::create_http_client;
5use miette::{Context, IntoDiagnostic, Result};
6use oauth2::{ClientId, ClientSecret, Scope, TokenResponse, TokenUrl, basic::BasicClient};
7
8/// An OAuth2 Client Credentials flow implementation for supported identity providers.
9///
10/// This flow is designed for machine-to-machine (M2M) authentication where
11/// no user interaction is required. It exchanges a client secret for an
12/// access token directly, without opening a browser.
13///
14/// # Provider Requirements
15///
16/// ## Microsoft Entra ID
17/// - The application must have **Application permissions** (not Delegated)
18/// - Admin consent must be granted for all requested scopes
19/// - Scopes must use the `.default` suffix (e.g. `api://my-api/.default`)
20///
21/// ## Auth0
22/// - Requires a dedicated **Machine to Machine** application — a Native app
23///   used for PKCE cannot use Client Credentials simultaneously
24/// - Under **Advanced Settings → Grant Types**, **Client Credentials** must be enabled
25/// - Scopes are explicit (e.g. `read:ez`) and must be granted under **APIs → Client Access**
26pub struct ClientCredentialsFlow {
27    /// The resolved identity provider with all required endpoint data.
28    pub provider: IdentityProvider,
29
30    /// The Application (Client) ID registered in Entra ID.
31    pub client_id: String,
32
33    /// The client secret for the registered application.
34    ///
35    /// Never persisted to disk — always sourced from a prompt or CLI argument.
36    pub client_secret: String,
37
38    /// The list of OAuth2 scopes to request.
39    ///
40    /// For Microsoft use `.default` suffix (e.g. `api://my-api/.default`).
41    /// For Auth0 use explicit scopes (e.g. `read:ez`).
42    pub scopes: Vec<String>,
43}
44
45impl Authenticator for ClientCredentialsFlow {
46    /// Exchanges the client credentials for an access token.
47    ///
48    /// Builds an OAuth2 client, adds the requested scopes, and performs
49    /// the token exchange against the provider's token endpoint.
50    /// For Auth0, an `audience` parameter is included automatically.
51    /// Displays a spinner during the request.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if:
56    /// - The provider produces an invalid token URL
57    /// - The HTTP client cannot be initialized
58    /// - The token exchange is rejected by the identity provider
59    async fn get_token(&self) -> Result<String> {
60        let token_uri = TokenUrl::new(self.provider.token_url())
61            .into_diagnostic()
62            .wrap_err("Invalid token URL")?;
63
64        let client = BasicClient::new(ClientId::new(self.client_id.clone()))
65            .set_client_secret(ClientSecret::new(self.client_secret.clone()))
66            .set_token_uri(token_uri);
67
68        let mut token_req = client.exchange_client_credentials();
69
70        if let Some(audience) = self.provider.audience() {
71            token_req = token_req.add_extra_param("audience", audience);
72        }
73
74        for scope in &self.scopes {
75            token_req = token_req.add_scope(Scope::new(scope.clone()));
76        }
77
78        let http_client = create_http_client()?;
79        let spinner = start_spinner("Authenticating...")?;
80
81        let token_result = token_req
82            .request_async(&http_client)
83            .await
84            .into_diagnostic()
85            .wrap_err("Failed to exchange Client Credentials for Access Token");
86
87        match token_result {
88            Ok(res) => {
89                finish_spinner_success(&spinner, "Authentication successful!");
90                Ok(res.access_token().secret().clone())
91            }
92            Err(e) => {
93                finish_spinner_error(&spinner, "Authentication failed!");
94                Err(e)
95            }
96        }
97    }
98}