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}