Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revise authentication challenge process #74

Merged
merged 5 commits into from
Sep 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ocipkg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ jobs:
ocipkg login -u ${{ github.repository_owner }} -p ${{ github.token }} https://ghcr.io
ocipkg get ghcr.io/termoshtt/ocipkg/dynamic/rust:1d23f83

get-anonymous:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v1
- uses: actions-rs/cargo@v1
with:
command: install
args: --path=ocipkg-cli/ -f
- name: Add path
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: get image from ghcr.io
run: |
ocipkg get ghcr.io/termoshtt/ocipkg/dynamic/rust:1d23f83

list:
runs-on: ubuntu-22.04
steps:
Expand Down
117 changes: 63 additions & 54 deletions ocipkg/src/distribution/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl StoredAuth {
Ok(())
}

/// Get token for using in API call
/// Get token by trying to access API root `/v2/`
///
/// Returns `None` if no authentication is required.
pub fn get_token(&self, url: &url::Url) -> Result<Option<String>> {
Expand All @@ -72,33 +72,31 @@ impl StoredAuth {
Err(ureq::Error::Transport(e)) => return Err(Error::NetworkError(e)),
};

let (ty, realm) = parse_www_authenticate_header(&www_auth);
if ty != "Bearer" {
log::warn!("Unsupported authenticate type, fallback: {}", ty);
return Ok(None);
}
let (token_url, query) = parse_bearer_realm(realm)?;
let challenge = AuthChallenge::from_header(&www_auth)?;
self.challenge(&challenge).map(|token| Some(token))
}

/// Get token based on WWW-Authentication header
pub fn challenge(&self, challenge: &AuthChallenge) -> Result<String> {
let token_url = Url::parse(&challenge.url)?;
let domain = token_url
.domain()
.expect("www-authenticate header returns invalid URL");

let mut req = ureq::get(token_url.as_str()).set("Accept", "application/json");
if let Some(auth) = self.auths.get(domain) {
let mut req = ureq::get(token_url.as_str())
.set("Authorization", &format!("Basic {}", auth.auth))
.set("Accept", "application/json");
for (k, v) in query {
req = req.query(k, v);
}
match req.call() {
Ok(res) => {
let token = res.into_json::<Token>()?;
Ok(Some(token.token))
}
Err(ureq::Error::Status(..)) => Err(Error::AuthorizationFailed(url.clone())),
Err(ureq::Error::Transport(e)) => Err(Error::NetworkError(e)),
req = req.set("Authorization", &format!("Basic {}", auth.auth))
}
req = req
.query("scope", &challenge.scope)
.query("service", &challenge.service);
match req.call() {
Ok(res) => {
let token = res.into_json::<Token>()?;
Ok(token.token)
}
} else {
Ok(None)
Err(ureq::Error::Status(..)) => Err(Error::AuthorizationFailed(token_url.clone())),
Err(ureq::Error::Transport(e)) => Err(Error::NetworkError(e)),
}
}

Expand Down Expand Up @@ -145,44 +143,55 @@ fn podman_auth_path() -> Option<PathBuf> {
Some(dirs.runtime_dir()?.join("auth.json"))
}

/// Parse the header of response. It must be in form:
/// WWW-Authentication challenge
///
/// ```text
/// WWW-Authenticate: <type> realm=<realm>
/// ```
/// use ocipkg::distribution::AuthChallenge;
///
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#www-authenticate_and_proxy-authenticate_headers
fn parse_www_authenticate_header(header: &str) -> (&str, &str) {
let re = regex::Regex::new(r"(\w+) realm=(.+)").unwrap();
let cap = re
.captures(header)
.expect("WWW-Authenticate header is invalid");
let ty = cap.get(1).unwrap().as_str();
let realm = cap.get(2).unwrap().as_str();
(ty, realm)
}

/// Parse realm
///
/// XXX: Where this format is defined?
///
/// ghcr.io returns following:
/// let auth = AuthChallenge::from_header(
/// r#"Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:termoshtt/ocipkg/rust-lib:pull""#,
/// ).unwrap();
///
/// ```text
/// Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:termoshtt/ocipkg/rust-lib:pull"
/// assert_eq!(auth, AuthChallenge {
/// url: "https://ghcr.io/token".to_string(),
/// service: "ghcr.io".to_string(),
/// scope: "repository:termoshtt/ocipkg/rust-lib:pull".to_string(),
/// });
/// ```
fn parse_bearer_realm(realm: &str) -> Result<(Url, Vec<(&str, &str)>)> {
let realm: Vec<_> = realm.split(',').collect();
assert!(!realm.is_empty());
let url = url::Url::parse(realm[0].trim_matches('"'))?;
let query: Vec<_> = realm[1..]
.iter()
.map(|param| {
let q: Vec<_> = param.split('=').collect();
(q[0].trim_matches('"'), q[1].trim_matches('"'))
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthChallenge {
pub url: String,
pub service: String,
pub scope: String,
}

impl AuthChallenge {
pub fn from_header(header: &str) -> Result<Self> {
let err = || Error::UnSupportedAuthHeader(header.to_string());
let (ty, realm) = header.split_once(' ').ok_or_else(err)?;
if ty != "Bearer" {
return Err(err());
}

let mut url = None;
let mut service = None;
let mut scope = None;
for param in realm.split(',') {
let (key, value) = param.split_once('=').ok_or_else(err)?;
let value = value.trim_matches('"').to_string();
match key {
"realm" => url = Some(value),
"service" => service = Some(value),
"scope" => scope = Some(value),
_ => continue,
}
}
Ok(Self {
url: url.ok_or_else(err)?,
service: service.ok_or_else(err)?,
scope: scope.ok_or_else(err)?,
})
.collect();
Ok((url, query))
}
}

#[derive(Deserialize)]
Expand Down