fix(registry): prevent token usage from unauthorized apps

This commit is contained in:
daimond113 2024-10-14 17:55:11 +02:00
parent 756b5c8257
commit 66a885b4e6
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
6 changed files with 57 additions and 24 deletions

View file

@ -9,15 +9,20 @@ COMMITTER_GIT_EMAIL= # email of the committer used for index updates
# AUTHENTICATION CONFIGURATION
# Set the variables of the authentication you want to use in order to enable it
READ_NEEDS_AUTH= # set to any value to require authentication for read requests
# Single Token
ACCESS_TOKEN= # a single token that is used to authenticate all publish requests
# Read/Write Tokens
# READ_NEEDS_AUTH isn't required for this
READ_ACCESS_TOKEN= # a token that is used to authenticate read requests
WRITE_ACCESS_TOKEN= # a token that is used to authenticate write requests
# GitHub
GITHUB_AUTH= # set to any value to enable GitHub authentication
GITHUB_CLIENT_SECRET= # client secret of the GitHub OAuth app configured in the index's `config.toml`
# If none of the above is set, no authentication is required, even for write requests

View file

@ -1,29 +1,51 @@
use crate::auth::{get_token_from_req, AuthImpl, UserId};
use actix_web::{dev::ServiceRequest, Error as ActixError};
use serde::Deserialize;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Debug)]
pub struct GitHubAuth {
pub reqwest_client: reqwest::Client,
pub client_id: String,
pub client_secret: String,
}
#[derive(Debug, Serialize)]
struct TokenRequestBody {
access_token: String,
}
impl AuthImpl for GitHubAuth {
async fn for_write_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> {
let token = match get_token_from_req(req, true) {
let token = match get_token_from_req(req) {
Some(token) => token,
None => return Ok(None),
};
let response = match self
.reqwest_client
.get("https://api.github.com/user")
.header(reqwest::header::AUTHORIZATION, token)
.post(format!(
"https://api.github.com/applications/{}/token",
self.client_id
))
.basic_auth(&self.client_id, Some(&self.client_secret))
.json(&TokenRequestBody {
access_token: token,
})
.send()
.await
.and_then(|res| res.error_for_status())
{
Ok(response) => response,
Ok(response) => match response.error_for_status_ref() {
Ok(_) => response,
Err(e) if e.status().is_some_and(|s| s == StatusCode::UNAUTHORIZED) => {
return Ok(None);
}
Err(e) => {
log::error!("failed to get user: {e}");
return Ok(None);
}
},
Err(e) => {
log::error!("failed to get user: {e}");
return Ok(None);
@ -31,7 +53,7 @@ impl AuthImpl for GitHubAuth {
};
let user_id = match response.json::<UserResponse>().await {
Ok(user) => user.id,
Ok(resp) => resp.user.id,
Err(e) => {
log::error!("failed to get user: {e}");
return Ok(None);
@ -49,6 +71,11 @@ impl Display for GitHubAuth {
}
#[derive(Debug, Deserialize)]
struct UserResponse {
struct User {
id: u64,
}
#[derive(Debug, Deserialize)]
struct UserResponse {
user: User,
}

View file

@ -13,6 +13,7 @@ use actix_web::{
middleware::Next,
web, HttpMessage, HttpResponse,
};
use pesde::{source::pesde::PesdePackageSource, Project};
use sha2::{Digest, Sha256};
use std::fmt::Display;
@ -55,7 +56,7 @@ pub trait AuthImpl: Display {
}
fn read_needs_auth(&self) -> bool {
false
benv!("READ_NEEDS_AUTH").is_ok()
}
}
@ -141,14 +142,20 @@ pub async fn read_mw(
next.call(req).await.map(|res| res.map_into_left_body())
}
pub fn get_auth_from_env() -> Auth {
pub fn get_auth_from_env(index: &PesdePackageSource, project: &Project) -> Auth {
if let Ok(token) = benv!("ACCESS_TOKEN") {
Auth::Token(token::TokenAuth {
token: *Sha256::digest(token.as_bytes()).as_ref(),
})
} else if benv!("GITHUB_AUTH").is_ok() {
let config = index.config(project).expect("failed to get index config");
Auth::GitHub(github::GitHubAuth {
reqwest_client: make_reqwest(),
client_id: config
.github_oauth_client_id
.expect("index isn't configured for GitHub"),
client_secret: benv!(required "GITHUB_CLIENT_SECRET"),
})
} else if let Ok((r, w)) =
benv!("READ_ACCESS_TOKEN").and_then(|r| benv!("WRITE_ACCESS_TOKEN").map(|w| (r, w)))
@ -162,7 +169,7 @@ pub fn get_auth_from_env() -> Auth {
}
}
pub fn get_token_from_req(req: &ServiceRequest, bearer: bool) -> Option<String> {
pub fn get_token_from_req(req: &ServiceRequest) -> Option<String> {
let token = match req
.headers()
.get(AUTHORIZATION)
@ -172,13 +179,7 @@ pub fn get_token_from_req(req: &ServiceRequest, bearer: bool) -> Option<String>
None => return None,
};
let token = if bearer {
if token.to_lowercase().starts_with("bearer ") {
token.to_string()
} else {
format!("Bearer {token}")
}
} else if token.to_lowercase().starts_with("bearer ") {
let token = if token.to_lowercase().starts_with("bearer ") {
token[7..].to_string()
} else {
token.to_string()

View file

@ -12,7 +12,7 @@ pub struct RwTokenAuth {
impl AuthImpl for RwTokenAuth {
async fn for_write_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> {
let token = match get_token_from_req(req, false) {
let token = match get_token_from_req(req) {
Some(token) => token,
None => return Ok(None),
};
@ -27,7 +27,7 @@ impl AuthImpl for RwTokenAuth {
}
async fn for_read_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> {
let token = match get_token_from_req(req, false) {
let token = match get_token_from_req(req) {
Some(token) => token,
None => return Ok(None),
};

View file

@ -12,7 +12,7 @@ pub struct TokenAuth {
impl AuthImpl for TokenAuth {
async fn for_write_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> {
let token = match get_token_from_req(req, false) {
let token = match get_token_from_req(req) {
Some(token) => token,
None => return Ok(None),
};

View file

@ -104,18 +104,18 @@ async fn run(with_sentry: bool) -> std::io::Result<()> {
let (search_reader, search_writer) = make_search(&project, &source);
let app_data = web::Data::new(AppState {
source: Mutex::new(source),
project,
storage: {
let storage = get_storage_from_env();
info!("storage: {storage}");
storage
},
auth: {
let auth = get_auth_from_env();
let auth = get_auth_from_env(&source, &project);
info!("auth: {auth}");
auth
},
source: Mutex::new(source),
project,
search_reader,
search_writer: Mutex::new(search_writer),