diff --git a/crates/uv-auth/src/lib.rs b/crates/uv-auth/src/lib.rs index 9f878b500..6aaf664ec 100644 --- a/crates/uv-auth/src/lib.rs +++ b/crates/uv-auth/src/lib.rs @@ -10,7 +10,9 @@ pub use credentials::{Credentials, Username}; pub use index::{AuthPolicy, Index, Indexes}; pub use keyring::KeyringProvider; pub use middleware::AuthMiddleware; -pub use pyx::{DEFAULT_TOLERANCE_SECS, PyxOAuthTokens, PyxTokenStore, PyxTokens, TokenStoreError}; +pub use pyx::{ + DEFAULT_TOLERANCE_SECS, PyxJwt, PyxOAuthTokens, PyxTokenStore, PyxTokens, TokenStoreError, +}; pub use realm::Realm; pub use service::{Service, ServiceParseError}; pub use store::{AuthBackend, AuthScheme, TextCredentialStore, TomlCredentialError}; diff --git a/crates/uv-auth/src/pyx.rs b/crates/uv-auth/src/pyx.rs index 00a5d6b3a..ba4889f2d 100644 --- a/crates/uv-auth/src/pyx.rs +++ b/crates/uv-auth/src/pyx.rs @@ -368,9 +368,9 @@ impl PyxTokenStore { tolerance_secs: u64, ) -> Result { // Decode the access token. - let jwt = Jwt::decode(match &tokens { - PyxTokens::OAuth(PyxOAuthTokens { access_token, .. }) => access_token.as_str(), - PyxTokens::ApiKey(PyxApiKeyTokens { access_token, .. }) => access_token.as_str(), + let jwt = PyxJwt::decode(match &tokens { + PyxTokens::OAuth(PyxOAuthTokens { access_token, .. }) => access_token, + PyxTokens::ApiKey(PyxApiKeyTokens { access_token, .. }) => access_token, })?; // If the access token is expired, refresh it. @@ -503,14 +503,20 @@ impl TokenStoreError { /// The payload of the JWT. #[derive(Debug, serde::Deserialize)] -struct Jwt { - exp: Option, +pub struct PyxJwt { + /// The expiration time of the JWT, as a Unix timestamp. + pub exp: Option, + /// The issuer of the JWT. + pub iss: Option, + /// The name of the organization, if any. + #[serde(rename = "urn:pyx:org_name")] + pub name: Option, } -impl Jwt { +impl PyxJwt { /// Decode the JWT from the access token. - fn decode(access_token: &str) -> Result { - let mut token_segments = access_token.splitn(3, '.'); + pub fn decode(access_token: &AccessToken) -> Result { + let mut token_segments = access_token.as_str().splitn(3, '.'); let _header = token_segments.next().ok_or(JwtError::MissingHeader)?; let payload = token_segments.next().ok_or(JwtError::MissingPayload)?; diff --git a/crates/uv/src/commands/auth/login.rs b/crates/uv/src/commands/auth/login.rs index 19ab50c9f..9fdc8d23d 100644 --- a/crates/uv/src/commands/auth/login.rs +++ b/crates/uv/src/commands/auth/login.rs @@ -7,8 +7,8 @@ use url::Url; use uuid::Uuid; use uv_auth::{ - AccessToken, AuthBackend, Credentials, PyxOAuthTokens, PyxTokenStore, PyxTokens, Service, - TextCredentialStore, + AccessToken, AuthBackend, Credentials, PyxJwt, PyxOAuthTokens, PyxTokenStore, PyxTokens, + Service, TextCredentialStore, }; use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder}; use uv_distribution_types::IndexUrl; @@ -45,12 +45,19 @@ pub(crate) async fn login( .auth_integration(AuthIntegration::NoAuthMiddleware) .build(); - pyx_login_with_browser(&pyx_store, &client, &printer).await?; - writeln!( - printer.stderr(), - "Logged in to {}", - pyx_store.api().bold().cyan() - )?; + let access_token = pyx_login_with_browser(&pyx_store, &client, &printer).await?; + let jwt = PyxJwt::decode(&access_token)?; + + if let Some(name) = jwt.name.as_deref() { + writeln!(printer.stderr(), "Logged in to {}", name.bold().cyan())?; + } else { + writeln!( + printer.stderr(), + "Logged in to {}", + pyx_store.api().bold().cyan() + )?; + } + return Ok(ExitStatus::Success); }