use crate::errors::{Result, SigstoreError};
use tracing::error;
use openidconnect::core::{
CoreClient, CoreIdToken, CoreIdTokenClaims, CoreIdTokenVerifier, CoreProviderMetadata,
CoreResponseType, CoreTokenResponse,
};
use openidconnect::reqwest::{async_http_client, http_client};
use openidconnect::{
AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce,
PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope,
};
use std::io::{BufRead, BufReader, Write};
use std::net::TcpListener;
use url::Url;
#[derive(Debug)]
pub struct OpenIDAuthorize {
oidc_cliend_id: String,
oidc_client_secret: String,
oidc_issuer: String,
redirect_url: String,
}
impl OpenIDAuthorize {
pub fn new(client_id: &str, client_secret: &str, issuer: &str, redirect_url: &str) -> Self {
Self {
oidc_cliend_id: client_id.to_string(),
oidc_client_secret: client_secret.to_string(),
oidc_issuer: issuer.to_string(),
redirect_url: redirect_url.to_string(),
}
}
fn auth_url_internal(
&self,
provider_metadata: CoreProviderMetadata,
) -> Result<(Url, CoreClient, Nonce, PkceCodeVerifier)> {
let client_id = ClientId::new(self.oidc_cliend_id.to_owned());
let client_secret = ClientSecret::new(self.oidc_client_secret.to_owned());
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let client =
CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret))
.set_redirect_uri(
RedirectUrl::new(self.redirect_url.to_owned()).expect("Invalid redirect URL"),
);
let (authorize_url, _, nonce) = client
.authorize_url(
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.add_scope(Scope::new("email".to_string()))
.set_pkce_challenge(pkce_challenge)
.url();
Ok((authorize_url, client, nonce, pkce_verifier))
}
pub fn auth_url(&self) -> Result<(Url, CoreClient, Nonce, PkceCodeVerifier)> {
let issuer = IssuerUrl::new(self.oidc_issuer.to_owned()).expect("Missing the OIDC_ISSUER.");
let provider_metadata =
CoreProviderMetadata::discover(&issuer, http_client).map_err(|err| {
error!("Error is: {:?}", err);
SigstoreError::ClaimsVerificationError
})?;
self.auth_url_internal(provider_metadata)
}
pub async fn auth_url_async(&self) -> Result<(Url, CoreClient, Nonce, PkceCodeVerifier)> {
let issuer = IssuerUrl::new(self.oidc_issuer.to_owned()).expect("Missing the OIDC_ISSUER.");
let provider_metadata = CoreProviderMetadata::discover_async(issuer, async_http_client)
.await
.map_err(|_| SigstoreError::ClaimsVerificationError)?;
self.auth_url_internal(provider_metadata)
}
}
pub struct RedirectListener {
client_redirect_host: String,
client: CoreClient,
nonce: Nonce,
pkce_verifier: PkceCodeVerifier,
}
impl RedirectListener {
pub fn new(
client_redirect_host: &str,
client: CoreClient,
nonce: Nonce,
pkce_verifier: PkceCodeVerifier,
) -> Self {
Self {
client_redirect_host: client_redirect_host.to_string(),
client,
nonce,
pkce_verifier,
}
}
fn redirect_listener_internal(&self) -> Result<AuthorizationCode> {
let listener = TcpListener::bind(self.client_redirect_host.clone())?;
#[allow(clippy::manual_flatten)]
for stream in listener.incoming() {
if let Ok(mut stream) = stream {
let code;
{
let mut reader = BufReader::new(&stream);
let mut request_line = String::new();
reader.read_line(&mut request_line)?;
let client_redirect_host = request_line
.split_whitespace()
.nth(1)
.ok_or(SigstoreError::RedirectUrlRequestLineError)?;
let url =
Url::parse(format!("http://localhost{}", client_redirect_host).as_str())?;
let code_pair = url
.query_pairs()
.find(|pair| {
let &(ref key, _) = pair;
key == "code"
})
.ok_or(SigstoreError::CodePairError)?;
let (_, value) = code_pair;
code = AuthorizationCode::new(value.into_owned());
}
let html_page = r#"<html>
<title>Sigstore Auth</title>
<body>
<h1>Sigstore Auth Successful</h1>
<p>You may now close this page.</p>
</body>
</html>"#;
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
html_page.len(),
html_page
);
stream.write_all(response.as_bytes())?;
return Ok(code);
}
}
Err(SigstoreError::CodePairError)
}
pub fn redirect_listener(self) -> Result<(CoreIdTokenClaims, CoreIdToken)> {
let code = self.redirect_listener_internal()?;
let token_response = self
.client
.exchange_code(code)
.set_pkce_verifier(self.pkce_verifier)
.request(http_client)
.map_err(|_| SigstoreError::ClaimsAccessPointError)?;
Self::extract_token_and_claims(
&token_response,
&self.client.id_token_verifier(),
self.nonce,
)
}
pub async fn redirect_listener_async(self) -> Result<(CoreIdTokenClaims, CoreIdToken)> {
let code = self.redirect_listener_internal()?;
let token_response = self
.client
.exchange_code(code)
.set_pkce_verifier(self.pkce_verifier)
.request_async(async_http_client)
.await
.map_err(|_| SigstoreError::ClaimsAccessPointError)?;
Self::extract_token_and_claims(
&token_response,
&self.client.id_token_verifier(),
self.nonce,
)
}
fn extract_token_and_claims(
token_response: &CoreTokenResponse,
id_token_verifier: &CoreIdTokenVerifier,
nonce: Nonce,
) -> Result<(CoreIdTokenClaims, CoreIdToken)> {
let id_token = token_response
.extra_fields()
.id_token()
.ok_or(SigstoreError::NoIDToken)?;
let id_token_claims: &CoreIdTokenClaims = token_response
.extra_fields()
.id_token()
.expect("Server did not return an ID token")
.claims(id_token_verifier, &nonce)
.map_err(|_| SigstoreError::ClaimsVerificationError)?;
Ok((id_token_claims.clone(), id_token.clone()))
}
}
#[test]
fn test_auth_url() {
let oidc_url = OpenIDAuthorize::new(
"sigstore",
"some_secret",
"https://oauth2.sigstore.dev/auth",
"http://localhost:8080",
)
.auth_url();
let oidc_url = oidc_url.unwrap();
assert!(oidc_url
.0
.to_string()
.contains("https://oauth2.sigstore.dev/auth"));
assert!(oidc_url.0.to_string().contains("response_type=code"));
assert!(oidc_url.0.to_string().contains("client_id=sigstore"));
assert!(oidc_url.0.to_string().contains("scope=openid+email"));
}