Skip to content

Commit f9c4f70

Browse files
JeadielukekimCopilotclaudespice
authored
Support OAuth2 client credentials in 'spice cloud login' (spiceai#10586)
* support OAuth2 client credentials in 'spice cloud login' * fix: address lint and security review issues in cloud login OAuth2 flow - Fix doc comment missing backticks on OAuth2 in client.rs - Replace fragile base_url string replace with oauth_base_url() helper that correctly handles both api.spice.ai and {env}-api.spice.ai hosts - Redact client_secret in LoginArgs Debug impl to prevent accidental leakage * formatting * fix: add backticks to doc comments for clippy::doc_markdown * fix: remove Debug derive from OAuth types containing secrets OAuthTokenRequest contains client_secret and OAuthTokenResponse contains access_token; deriving Debug risks accidental credential disclosure. * feat(cloud): enhance login methods and OAuth token handling * feat(cloud): update login methods and enhance documentation for cloud login command Co-authored-by: Copilot <copilot@github.com> * fix(docs): adjust formatting in cloud login documentation table * feat(cloud): improve login handling and add tests for login methods * fix(cloud): correct documentation formatting and improve item selection handling in login prompt * docs(cloud-login): apply review feedback fixes - Remove `spice cloud login token` and `spice cloud login client` alias examples, since the corresponding Clap subcommands do not declare aliases. - Reconcile the OAuth host resolution section with the wire formats: the exchange polling endpoint (`/v1/auth/device/exchange`) uses the API base URL, not the non-API OAuth host. - Replace "fifth method" with "another method" since the doc currently describes three methods. Addresses copilot review feedback. * fix(cloud-login): resolve env vars in chooser path The chooser path constructs PatLoginArgs / ApiLoginArgs with all fields set to None, bypassing Clap's env-var resolution. Users with SPICE_CLOUD_PAT, SPICE_CLOUD_CLIENT_ID, or SPICE_CLOUD_CLIENT_SECRET set were prompted interactively even when the env vars contained valid values. `resolve_string_or_prompt_with_terminal` now falls back to reading the configured env var directly before prompting, so chooser-based PAT/API logins respect env configuration consistently with the explicit-method paths. Adds a unit test exercising the env-var fallback. Addresses copilot review feedback. * style: collapse nested if to satisfy clippy::collapsible_if Combines the env-var read and non-empty check into a single `if let && condition` form to match the repo's clippy::pedantic config. --------- Co-authored-by: Luke Kim <80174+lukekim@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com> Co-authored-by: claudespice <claude@spice.ai>
1 parent 3068bb6 commit f9c4f70

6 files changed

Lines changed: 935 additions & 38 deletions

File tree

bin/spice/src/commands/cloud/client.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ impl CloudClient {
4444
/// Create a new authenticated cloud client.
4545
pub fn new() -> Result<Self> {
4646
let token = get_auth_token()?;
47+
Self::with_token(token)
48+
}
49+
50+
/// Create a new authenticated cloud client with an explicit bearer token.
51+
pub fn with_token(token: impl Into<String>) -> Result<Self> {
4752
Ok(Self {
4853
inner: InnerCloudClient::new(&get_base_url())
4954
.map_err(into_cli)?
@@ -68,6 +73,31 @@ impl CloudClient {
6873
self.inner.exchange_code(auth_code).await.map_err(into_cli)
6974
}
7075

76+
/// Exchange `OAuth2` client credentials for an access token.
77+
pub async fn exchange_client_credentials(
78+
&self,
79+
client_id: &str,
80+
client_secret: &str,
81+
) -> Result<String> {
82+
let response = self
83+
.inner
84+
.exchange_client_credentials(client_id, client_secret)
85+
.await
86+
.map_err(into_cli)?;
87+
88+
if response.token_type.eq_ignore_ascii_case("bearer") {
89+
Ok(response.access_token)
90+
} else {
91+
InvalidResponseSnafu {
92+
message: format!(
93+
"Failed to exchange client credentials: unsupported OAuth token type '{}'; expected 'Bearer'",
94+
response.token_type
95+
),
96+
}
97+
.fail()
98+
}
99+
}
100+
71101
/// Get the auth context for the current user.
72102
pub async fn get_auth_context(&self) -> Result<AuthContext> {
73103
self.inner.get_auth_context().await.map_err(into_cli)

0 commit comments

Comments
 (0)