Render token claims on publish permission error (#12135)

Match the official trusted publishing GitHub Action from
db8f07d387/oidc-exchange.py (L165-L184)

See
https://github.com/konstin/uv/actions/runs/13812003071/job/38635620817?pr=3
for an example of how this renders
This commit is contained in:
konsti 2025-03-13 11:19:08 +01:00 committed by GitHub
parent be87255539
commit b4eabf9a61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 52 additions and 10 deletions

View File

@ -1,5 +1,7 @@
//! Trusted publishing (via OIDC) with GitHub actions. //! Trusted publishing (via OIDC) with GitHub actions.
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use base64::Engine;
use reqwest::{header, StatusCode}; use reqwest::{header, StatusCode};
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -27,9 +29,12 @@ pub enum TrustedPublishingError {
#[error(transparent)] #[error(transparent)]
SerdeJson(#[from] serde_json::error::Error), SerdeJson(#[from] serde_json::error::Error),
#[error( #[error(
"PyPI returned error code {0}, is trusted publishing correctly configured?\nResponse: {1}" "PyPI returned error code {0}, is trusted publishing correctly configured?\nResponse: {1}\nToken claims, which must match the PyPI configuration: {2:#?}"
)] )]
Pypi(StatusCode, String), Pypi(StatusCode, String, OidcTokenClaims),
/// When trusted publishing is misconfigured, the error above should occur, not this one.
#[error("PyPI returned error code {0}, and the OIDC has an unexpected format.\nResponse: {1}")]
InvalidOidcToken(StatusCode, String),
} }
impl TrustedPublishingError { impl TrustedPublishingError {
@ -75,6 +80,18 @@ struct PublishToken {
token: TrustedPublishingToken, token: TrustedPublishingToken,
} }
/// The payload of the OIDC token.
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct OidcTokenClaims {
sub: String,
repository: String,
repository_owner: String,
repository_owner_id: String,
job_workflow_ref: String,
r#ref: String,
}
/// Returns the short-lived token to use for uploading. /// Returns the short-lived token to use for uploading.
pub(crate) async fn get_token( pub(crate) async fn get_token(
registry: &Url, registry: &Url,
@ -158,6 +175,18 @@ async fn get_oidc_token(
Ok(oidc_token.value) Ok(oidc_token.value)
} }
/// Parse the JSON Web Token that the OIDC token is.
///
/// See: <https://github.com/pypa/gh-action-pypi-publish/blob/db8f07d3871a0a180efa06b95d467625c19d5d5f/oidc-exchange.py#L165-L184>
fn decode_oidc_token(oidc_token: &str) -> Option<OidcTokenClaims> {
let token_segments = oidc_token.splitn(3, '.').collect::<Vec<&str>>();
let [_header, payload, _signature] = *token_segments.into_boxed_slice() else {
return None;
};
let decoded = BASE64_URL_SAFE_NO_PAD.decode(payload).ok()?;
serde_json::from_slice(&decoded).ok()
}
async fn get_publish_token( async fn get_publish_token(
registry: &Url, registry: &Url,
oidc_token: &str, oidc_token: &str,
@ -189,13 +218,26 @@ async fn get_publish_token(
let publish_token: PublishToken = serde_json::from_slice(&body)?; let publish_token: PublishToken = serde_json::from_slice(&body)?;
Ok(publish_token.token) Ok(publish_token.token)
} else { } else {
match decode_oidc_token(oidc_token) {
Some(claims) => {
// An error here means that something is misconfigured, e.g. a typo in the PyPI // An error here means that something is misconfigured, e.g. a typo in the PyPI
// configuration, so we're showing the body for more context, see // configuration, so we're showing the body and the JWT claims for more context, see
// https://docs.pypi.org/trusted-publishers/troubleshooting/#token-minting // https://docs.pypi.org/trusted-publishers/troubleshooting/#token-minting
// for what the body can mean. // for what the body can mean.
Err(TrustedPublishingError::Pypi( Err(TrustedPublishingError::Pypi(
status, status,
String::from_utf8_lossy(&body).to_string(), String::from_utf8_lossy(&body).to_string(),
claims,
)) ))
} }
None => {
// This is not a user configuration error, the OIDC token should always have a valid
// format.
Err(TrustedPublishingError::InvalidOidcToken(
status,
String::from_utf8_lossy(&body).to_string(),
))
}
}
}
} }