Use bearer tokens instead of embedded URL credentials on the web API
Diff
chartered-web/src/main.rs | 19 +++++++++++++------
chartered-frontend/src/stores/auth.ts | 22 +++++++++++++---------
chartered-web/src/middleware/auth.rs | 89 --------------------------------------------------------------------------------
chartered-web/src/middleware/cargo_auth.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
chartered-web/src/middleware/mod.rs | 3 ++-
chartered-web/src/middleware/web_auth.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
chartered-web/src/endpoints/web_api/ssh_key.rs | 2 +-
chartered-frontend/src/routes/(authed)/ssh-keys/CreateKeyForm.svelte | 4 +++-
chartered-frontend/src/routes/(authed)/ssh-keys/DeleteSshKeyModal.svelte | 6 +++++-
chartered-web/src/endpoints/web_api/crates/info.rs | 2 +-
chartered-web/src/endpoints/web_api/crates/members.rs | 8 ++++----
chartered-web/src/endpoints/web_api/organisations/info.rs | 2 +-
chartered-web/src/endpoints/web_api/organisations/members.rs | 6 +++---
chartered-web/src/endpoints/web_api/users/heatmap.rs | 2 +-
chartered-web/src/endpoints/web_api/users/info.rs | 2 +-
chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte | 8 +++++---
chartered-frontend/src/routes/(authed)/organisations/create/+page.svelte | 4 +++-
chartered-frontend/src/routes/(authed)/sessions/list/DeleteSessionModal.svelte | 4 +++-
18 files changed, 264 insertions(+), 124 deletions(-)
@@ -71,19 +71,22 @@
let app = Router::new()
.route("/", get(hello_world))
.nest(
"/a/:key/web/v1",
"/web/v1",
endpoints::web_api::authenticated_routes().layer(
ServiceBuilder::new()
.layer_fn(crate::middleware::auth::AuthMiddleware)
.layer_fn(crate::middleware::web_auth::WebAuthMiddleware)
.into_inner(),
),
)
.nest("/a/-/web/v1", endpoints::web_api::unauthenticated_routes())
.nest(
"/web/v1/public",
endpoints::web_api::unauthenticated_routes(),
)
.nest(
"/a/:key/o/:organisation/api/v1",
endpoints::cargo_api::routes().layer(
ServiceBuilder::new()
.layer_fn(crate::middleware::auth::AuthMiddleware)
.layer_fn(crate::middleware::cargo_auth::CargoAuthMiddleware)
.into_inner(),
),
)
@@ -97,8 +100,12 @@
Method::DELETE,
Method::PUT,
Method::OPTIONS,
])
.allow_headers(vec![
header::CONTENT_TYPE,
header::USER_AGENT,
header::AUTHORIZATION,
])
.allow_headers(vec![header::CONTENT_TYPE, header::USER_AGENT])
.allow_origin(AllowOrigin::predicate({
let config = config.clone();
move |url, _| {
@@ -109,7 +116,7 @@
.unwrap_or_default()
}
}))
.allow_credentials(false),
.allow_credentials(true),
)
.layer(Extension(pool))
.layer(Extension(Arc::new(config.create_oidc_clients().await?)))
@@ -67,8 +67,7 @@
try {
const result = await fetch(`${BASE_URL}/a/${currentAuth.auth_key}/web/v1/auth/extend`);
const json: ExtendResult = await result.json();
const json = await request<ExtendResult>(`/web/v1/auth/extend`);
if (json.error) {
@@ -124,7 +123,7 @@
*/
export async function login(username: string, password: string) {
const result = await fetch(`${BASE_URL}/a/-/web/v1/auth/login/password`, {
const result = await fetch(`${BASE_URL}/web/v1/public/auth/login/password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
@@ -153,7 +152,7 @@
*/
export async function handleOAuthCallback(params: string) {
const result = await fetch(`${BASE_URL}/a/-/web/v1/auth/login/oauth/complete${params}`);
const result = await fetch(`${BASE_URL}/web/v1/public/auth/login/oauth/complete${params}`);
const json: LoginResult = await result.json();
@@ -192,7 +191,12 @@
throw new Error('Not authenticated');
}
const result = await fetch(`${BASE_URL}/a/${token}${url}`);
const result = await fetch(`${BASE_URL}${url}`, {
headers: {
Authorization: `Bearer ${token}`,
},
credentials: 'include',
});
const json: T & Error = await result.json();
@@ -209,7 +213,7 @@
* @param provider OAuth provider as configured on the backend to grab an auth link for
*/
export async function loginOAuth(provider: string) {
const result = await fetch(`${BASE_URL}/a/-/web/v1/auth/login/oauth/${provider}/begin`);
const result = await fetch(`${BASE_URL}/web/v1/public/auth/login/oauth/${provider}/begin`);
const json: LoginOAuthResult = await result.json();
if (json.error) {
@@ -231,7 +235,7 @@
const authKey = get(auth)?.auth_key;
if (authKey) {
await fetch(`${BASE_URL}/a/${authKey}/web/v1/auth/logout`);
await request<boolean>(`/web/v1/auth/logout`);
}
} catch (e) {
console.error('Failed to fully log user out of session', e);
@@ -253,7 +257,7 @@
* Grab all the possible authentication methods from the backend.
*/
export async function fetchOAuthProviders(): Promise<OAuthProviders> {
const result = await fetch(`${BASE_URL}/a/-/web/v1/auth/login/oauth/providers`);
const result = await fetch(`${BASE_URL}/web/v1/public/auth/login/oauth/providers`);
return await result.json();
}
@@ -274,7 +278,7 @@
*/
export async function register(username: string, password: string) {
const result = await fetch(`${BASE_URL}/a/-/web/v1/auth/register/password`, {
const result = await fetch(`${BASE_URL}/web/v1/public/auth/register/password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1,89 +1,0 @@
use axum::{
body::{boxed, Body, BoxBody},
extract::{self, FromRequest, RequestParts},
http::{Request, Response, StatusCode},
};
use chartered_db::users::User;
use chartered_db::ConnectionPool;
use futures::future::BoxFuture;
use std::sync::Arc;
use std::{
collections::HashMap,
task::{Context, Poll},
};
use tower::Service;
use crate::endpoints::ErrorResponse;
#[derive(Clone)]
pub struct AuthMiddleware<S>(pub S);
impl<S, ReqBody> Service<Request<ReqBody>> for AuthMiddleware<S>
where
S: Service<Request<ReqBody>, Response = Response<BoxBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.0.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let clone = self.0.clone();
let mut inner = std::mem::replace(&mut self.0, clone);
Box::pin(async move {
let mut req = RequestParts::new(req);
let params = extract::Path::<HashMap<String, String>>::from_request(&mut req)
.await
.unwrap();
let key = params.get("key").map(String::as_str).unwrap_or_default();
let db = req.extensions().get::<ConnectionPool>().unwrap().clone();
let (session, user) = match User::find_by_session_key(db, String::from(key))
.await
.unwrap()
{
Some((session, user)) => (Arc::new(session), Arc::new(user)),
None => {
return Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(boxed(Body::from(
serde_json::to_vec(&ErrorResponse {
error: Some("Expired auth token".into()),
})
.unwrap(),
)))
.unwrap())
}
};
req.extensions_mut().insert(user);
req.extensions_mut().insert(session);
let response: Response<BoxBody> = inner.call(req.try_into_request().unwrap()).await?;
Ok(response)
})
}
}
@@ -1,0 +1,101 @@
use axum::{
body::{boxed, Body, BoxBody},
extract::{self, FromRequest, RequestParts},
http::{Request, Response, StatusCode},
};
use chartered_db::{users::User, ConnectionPool};
use futures::future::BoxFuture;
use std::{
collections::HashMap,
sync::Arc,
task::{Context, Poll},
};
use tower::Service;
use crate::endpoints::ErrorResponse;
#[derive(Clone)]
pub struct CargoAuthMiddleware<S>(pub S);
impl<S, ReqBody> Service<Request<ReqBody>> for CargoAuthMiddleware<S>
where
S: Service<Request<ReqBody>, Response = Response<BoxBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.0.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let clone = self.0.clone();
let mut inner = std::mem::replace(&mut self.0, clone);
Box::pin(async move {
let mut req = RequestParts::new(req);
let params = extract::Path::<HashMap<String, String>>::from_request(&mut req)
.await
.unwrap();
let key = params.get("key").map(String::as_str).unwrap_or_default();
let db = req.extensions().get::<ConnectionPool>().unwrap().clone();
let (session, user) = match User::find_by_session_key(db, String::from(key))
.await
.unwrap()
{
Some((session, user)) => (Arc::new(session), Arc::new(user)),
None => {
return Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(boxed(Body::from(
serde_json::to_vec(&ErrorResponse {
error: Some("Expired auth token".into()),
})
.unwrap(),
)))
.unwrap())
}
};
if session.user_ssh_key_id.is_none() {
return Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(boxed(Body::from(
serde_json::to_vec(&ErrorResponse {
error: Some("Invalid auth token".into()),
})
.unwrap(),
)))
.unwrap());
}
req.extensions_mut().insert(user);
req.extensions_mut().insert(session);
let response: Response<BoxBody> = inner.call(req.try_into_request().unwrap()).await?;
Ok(response)
})
}
}
@@ -1,2 +1,3 @@
pub mod auth;
pub mod cargo_auth;
pub mod logging;
pub mod web_auth;
@@ -1,0 +1,104 @@
use axum::{
body::{boxed, Body, BoxBody},
extract::{self, FromRequest, RequestParts},
http::{Request, Response, StatusCode},
response::IntoResponse,
TypedHeader,
};
use chartered_db::{users::User, ConnectionPool};
use futures::future::BoxFuture;
use headers::{authorization::Bearer, Authorization};
use std::{
sync::Arc,
task::{Context, Poll},
};
use tower::Service;
use crate::endpoints::ErrorResponse;
#[derive(Clone)]
pub struct WebAuthMiddleware<S>(pub S);
impl<S, ReqBody> Service<Request<ReqBody>> for WebAuthMiddleware<S>
where
S: Service<Request<ReqBody>, Response = Response<BoxBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.0.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let clone = self.0.clone();
let mut inner = std::mem::replace(&mut self.0, clone);
Box::pin(async move {
let mut req = RequestParts::new(req);
let authorization: Authorization<Bearer> =
match extract::TypedHeader::from_request(&mut req).await {
Ok(TypedHeader(v)) => v,
Err(e) => return Ok(e.into_response()),
};
let db = req.extensions().get::<ConnectionPool>().unwrap().clone();
let (session, user) =
match User::find_by_session_key(db, String::from(authorization.0.token()))
.await
.unwrap()
{
Some((session, user)) => (Arc::new(session), Arc::new(user)),
None => {
return Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(boxed(Body::from(
serde_json::to_vec(&ErrorResponse {
error: Some("Expired auth token".into()),
})
.unwrap(),
)))
.unwrap())
}
};
if session.user_ssh_key_id.is_some() {
return Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(boxed(Body::from(
serde_json::to_vec(&ErrorResponse {
error: Some("Invalid auth token".into()),
})
.unwrap(),
)))
.unwrap());
}
req.extensions_mut().insert(user);
req.extensions_mut().insert(session);
let response: Response<BoxBody> = inner.call(req.try_into_request().unwrap()).await?;
Ok(response)
})
}
}
@@ -53,7 +53,7 @@
pub async fn handle_delete(
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Path((_session_key, ssh_key_id)): extract::Path<(String, Uuid)>,
extract::Path(ssh_key_id): extract::Path<Uuid>,
) -> Result<Json<ErrorResponse>, Error> {
let deleted = user.delete_user_ssh_key_by_uuid(db, ssh_key_id).await?;
@@ -41,12 +41,14 @@
try {
// submit the key to the backend
let result = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/web/v1/ssh-key`, {
let result = await fetch(`${BASE_URL}/web/v1/ssh-key`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${$auth?.auth_key}`,
},
body: JSON.stringify({ key: sshKey }),
credentials: 'include',
});
let json: AddSshKeyResult = await result.json();
@@ -49,8 +49,12 @@
try {
// submit deletion request to backend
let res = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/web/v1/ssh-key/${deleting.uuid}`, {
let res = await fetch(`${BASE_URL}/web/v1/ssh-key/${deleting.uuid}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${$auth?.auth_key}`,
},
credentials: 'include',
});
let json: DeleteSshKeyResult = await res.json();
@@ -14,7 +14,7 @@
use thiserror::Error;
pub async fn handle(
extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>,
extract::Path((organisation, name)): extract::Path<(String, String)>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<axum::response::Response, Error> {
@@ -16,7 +16,7 @@
pub async fn handle_get(
extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>,
extract::Path((organisation, name)): extract::Path<(String, String)>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Json<GetResponse>, Error> {
@@ -44,7 +44,7 @@
pub async fn handle_patch(
extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>,
extract::Path((organisation, name)): extract::Path<(String, String)>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<PutOrPatchRequest>,
@@ -68,7 +68,7 @@
pub async fn handle_put(
extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>,
extract::Path((organisation, name)): extract::Path<(String, String)>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<PutOrPatchRequest>,
@@ -89,7 +89,7 @@
pub async fn handle_delete(
extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>,
extract::Path((organisation, name)): extract::Path<(String, String)>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<DeleteRequest>,
@@ -11,7 +11,7 @@
use thiserror::Error;
pub async fn handle_get(
extract::Path((_session_key, organisation)): extract::Path<(String, String)>,
extract::Path(organisation): extract::Path<String>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Json<Response>, Error> {
@@ -13,7 +13,7 @@
pub async fn handle_patch(
extract::Path((_session_key, organisation)): extract::Path<(String, String)>,
extract::Path(organisation): extract::Path<String>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<PutOrPatchRequest>,
@@ -37,7 +37,7 @@
pub async fn handle_put(
extract::Path((_session_key, organisation)): extract::Path<(String, String)>,
extract::Path(organisation): extract::Path<String>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<PutOrPatchRequest>,
@@ -58,7 +58,7 @@
pub async fn handle_delete(
extract::Path((_session_key, organisation)): extract::Path<(String, String)>,
extract::Path(organisation): extract::Path<String>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<DeleteRequest>,
@@ -6,7 +6,7 @@
use thiserror::Error;
pub async fn handle(
extract::Path((_session_key, uuid)): extract::Path<(String, chartered_db::uuid::Uuid)>,
extract::Path(uuid): extract::Path<chartered_db::uuid::Uuid>,
extract::Extension(db): extract::Extension<ConnectionPool>,
) -> Result<Json<Response>, Error> {
let user = User::find_by_uuid(db.clone(), uuid)
@@ -9,7 +9,7 @@
use thiserror::Error;
pub async fn handle(
extract::Path((_session_key, uuid)): extract::Path<(String, chartered_db::uuid::Uuid)>,
extract::Path(uuid): extract::Path<chartered_db::uuid::Uuid>,
extract::Extension(db): extract::Extension<ConnectionPool>,
) -> Result<Json<Response>, Error> {
let user = User::find_by_uuid(db, uuid).await?.ok_or(Error::NotFound)?;
@@ -106,22 +106,24 @@
// out which one we need to persist the changes to...
let url;
if (crate) {
url = `web/v1/crates/${organisation}/${crate}`;
url = `crates/${organisation}/${crate}`;
} else {
url = `web/v1/organisations/${organisation}`;
url = `organisations/${organisation}`;
}
// send the membership update to the backend
let result = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/${url}/members`, {
let result = await fetch(`${BASE_URL}/web/v1/${url}/members`, {
method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${$auth?.auth_key}`,
},
body: JSON.stringify({
user_uuid: member.uuid,
permissions: newPermissions,
}),
credentials: 'include',
});
let json = await result.json();
@@ -44,12 +44,14 @@
try {
// attempt the actual creation of the organisation
let result = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/web/v1/organisations`, {
let result = await fetch(`${BASE_URL}/web/v1/organisations`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${$auth?.auth_key}`,
},
body: JSON.stringify({ name, description, public: isPublic }),
credentials: 'include',
});
let json = await result.json();
@@ -48,12 +48,14 @@
try {
// submit deletion request to backend
let res = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/web/v1/sessions`, {
let res = await fetch(`${BASE_URL}/web/v1/sessions`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${$auth?.auth_key}`,
},
body: JSON.stringify({ uuid: deleting.uuid }),
credentials: 'include',
});
let json: { error?: string } = await res.json();