🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2022-09-17 4:38:49.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-09-17 4:38:49.0 +01:00:00
commit
aae650877f16b744e509057eb75d2ec3324f2e94 [patch]
tree
0a841cc35d9a5601dca476218cec6f590949dcd5
parent
94633dbc97ab9597b77a4db7f10a15cbde7a0c9d
download
aae650877f16b744e509057eb75d2ec3324f2e94.tar.gz

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(-)

diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs
index edfb60e..42dae1b 100644
--- a/chartered-web/src/main.rs
+++ a/chartered-web/src/main.rs
@@ -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?)))
diff --git a/chartered-frontend/src/stores/auth.ts b/chartered-frontend/src/stores/auth.ts
index 15474d5..77e6295 100644
--- a/chartered-frontend/src/stores/auth.ts
+++ a/chartered-frontend/src/stores/auth.ts
@@ -67,8 +67,7 @@

    try {
        // call chartered-web to attempt to extend the session
        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`);

        // backend returned an error, nothing we can do here
        if (json.error) {
@@ -124,7 +123,7 @@
 */
export async function login(username: string, password: string) {
    // call the backend and attempt the authentication
    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) {
    // call the backend and attempt the authentication
    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();

    // server returned an error, forward it on - there's nothing else we
@@ -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();

    // TODO: handle 404s
@@ -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) {
    // send register request to backend
    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',
diff --git a/chartered-web/src/middleware/auth.rs b/chartered-web/src/middleware/auth.rs
deleted file mode 100644
index 98e6aad..0000000 100644
--- a/chartered-web/src/middleware/auth.rs
+++ /dev/null
@@ -1,89 +1,0 @@
//! Check the API key embedded in the path is valid otherwise returns a 401 for authenticated

//! endpoints.


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 {
        // best practice is to clone the inner service like this
        // see https://github.com/tower-rs/tower/issues/547 for details
        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);

            // extracts all parameters from the path so we can get the API key which should
            // always be named key
            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();

            // grab the ConnectionPool from the extensions created when we initialised the
            // server
            let db = req.extensions().get::<ConnectionPool>().unwrap().clone();

            // grab the UserSession that's currently being used for this request and the User that
            // owns the key, otherwise return a 401 if the key doesn't exist
            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())
                }
            };

            // insert both the user and the session into extensions so handlers can
            // get their hands on them
            req.extensions_mut().insert(user);
            req.extensions_mut().insert(session);

            // calls handlers/other middleware and drives the request to response
            let response: Response<BoxBody> = inner.call(req.try_into_request().unwrap()).await?;

            Ok(response)
        })
    }
}
diff --git a/chartered-web/src/middleware/cargo_auth.rs b/chartered-web/src/middleware/cargo_auth.rs
new file mode 100644
index 0000000..38c1c15 100644
--- /dev/null
+++ a/chartered-web/src/middleware/cargo_auth.rs
@@ -1,0 +1,101 @@
//! Check the API key embedded in the path is valid otherwise returns a 401 for authenticated

//! endpoints.


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 {
        // best practice is to clone the inner service like this
        // see https://github.com/tower-rs/tower/issues/547 for details
        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);

            // extracts all parameters from the path so we can get the API key which should
            // always be named key
            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();

            // grab the ConnectionPool from the extensions created when we initialised the
            // server
            let db = req.extensions().get::<ConnectionPool>().unwrap().clone();

            // grab the UserSession that's currently being used for this request and the User that
            // owns the key, otherwise return a 401 if the key doesn't exist
            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() {
                // Web sessions can't be used for the Cargo API
                return Ok(Response::builder()
                    .status(StatusCode::UNAUTHORIZED)
                    .body(boxed(Body::from(
                        serde_json::to_vec(&ErrorResponse {
                            error: Some("Invalid auth token".into()),
                        })
                        .unwrap(),
                    )))
                    .unwrap());
            }

            // insert both the user and the session into extensions so handlers can
            // get their hands on them
            req.extensions_mut().insert(user);
            req.extensions_mut().insert(session);

            // calls handlers/other middleware and drives the request to response
            let response: Response<BoxBody> = inner.call(req.try_into_request().unwrap()).await?;

            Ok(response)
        })
    }
}
diff --git a/chartered-web/src/middleware/mod.rs b/chartered-web/src/middleware/mod.rs
index 7eaa040..d452bf7 100644
--- a/chartered-web/src/middleware/mod.rs
+++ a/chartered-web/src/middleware/mod.rs
@@ -1,2 +1,3 @@
pub mod auth;
pub mod cargo_auth;
pub mod logging;
pub mod web_auth;
diff --git a/chartered-web/src/middleware/web_auth.rs b/chartered-web/src/middleware/web_auth.rs
new file mode 100644
index 0000000..ad64804 100644
--- /dev/null
+++ a/chartered-web/src/middleware/web_auth.rs
@@ -1,0 +1,104 @@
//! Check the API key in the authorization header is valid otherwise returns a 401 for authenticated

//! endpoints.


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 {
        // best practice is to clone the inner service like this
        // see https://github.com/tower-rs/tower/issues/547 for details
        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);

            // extract the authorization header
            let authorization: Authorization<Bearer> =
                match extract::TypedHeader::from_request(&mut req).await {
                    Ok(TypedHeader(v)) => v,
                    Err(e) => return Ok(e.into_response()),
                };

            // grab the ConnectionPool from the extensions created when we initialised the
            // server
            let db = req.extensions().get::<ConnectionPool>().unwrap().clone();

            // grab the UserSession that's currently being used for this request and the User that
            // owns the key, otherwise return a 401 if the key doesn't exist
            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() {
                // SSH sessions can't be used for the web API
                return Ok(Response::builder()
                    .status(StatusCode::UNAUTHORIZED)
                    .body(boxed(Body::from(
                        serde_json::to_vec(&ErrorResponse {
                            error: Some("Invalid auth token".into()),
                        })
                        .unwrap(),
                    )))
                    .unwrap());
            }

            // insert both the user and the session into extensions so handlers can
            // get their hands on them
            req.extensions_mut().insert(user);
            req.extensions_mut().insert(session);

            // calls handlers/other middleware and drives the request to response
            let response: Response<BoxBody> = inner.call(req.try_into_request().unwrap()).await?;

            Ok(response)
        })
    }
}
diff --git a/chartered-web/src/endpoints/web_api/ssh_key.rs b/chartered-web/src/endpoints/web_api/ssh_key.rs
index e67b9b9..65d83e2 100644
--- a/chartered-web/src/endpoints/web_api/ssh_key.rs
+++ a/chartered-web/src/endpoints/web_api/ssh_key.rs
@@ -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?;

diff --git a/chartered-frontend/src/routes/(authed)/ssh-keys/CreateKeyForm.svelte b/chartered-frontend/src/routes/(authed)/ssh-keys/CreateKeyForm.svelte
index 07e3e6a..ff91025 100644
--- a/chartered-frontend/src/routes/(authed)/ssh-keys/CreateKeyForm.svelte
+++ a/chartered-frontend/src/routes/(authed)/ssh-keys/CreateKeyForm.svelte
@@ -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();

diff --git a/chartered-frontend/src/routes/(authed)/ssh-keys/DeleteSshKeyModal.svelte b/chartered-frontend/src/routes/(authed)/ssh-keys/DeleteSshKeyModal.svelte
index 9e34b34..e59a56b 100644
--- a/chartered-frontend/src/routes/(authed)/ssh-keys/DeleteSshKeyModal.svelte
+++ a/chartered-frontend/src/routes/(authed)/ssh-keys/DeleteSshKeyModal.svelte
@@ -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();

diff --git a/chartered-web/src/endpoints/web_api/crates/info.rs b/chartered-web/src/endpoints/web_api/crates/info.rs
index 59ed0e2..eb6eaf3 100644
--- a/chartered-web/src/endpoints/web_api/crates/info.rs
+++ a/chartered-web/src/endpoints/web_api/crates/info.rs
@@ -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> {
diff --git a/chartered-web/src/endpoints/web_api/crates/members.rs b/chartered-web/src/endpoints/web_api/crates/members.rs
index f84969a..a4acedb 100644
--- a/chartered-web/src/endpoints/web_api/crates/members.rs
+++ a/chartered-web/src/endpoints/web_api/crates/members.rs
@@ -16,7 +16,7 @@
///

/// These members could be specific to the crate or they could be overrides ontop of the org.

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 @@

/// Updates a crate member's permissions

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 @@

/// Inserts an permissions override for this crate for a specific user

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 @@

/// Deletes a member override from this crate

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>,
diff --git a/chartered-web/src/endpoints/web_api/organisations/info.rs b/chartered-web/src/endpoints/web_api/organisations/info.rs
index af2fdac..003e68c 100644
--- a/chartered-web/src/endpoints/web_api/organisations/info.rs
+++ a/chartered-web/src/endpoints/web_api/organisations/info.rs
@@ -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> {
diff --git a/chartered-web/src/endpoints/web_api/organisations/members.rs b/chartered-web/src/endpoints/web_api/organisations/members.rs
index 60c4b78..97cb8cd 100644
--- a/chartered-web/src/endpoints/web_api/organisations/members.rs
+++ a/chartered-web/src/endpoints/web_api/organisations/members.rs
@@ -13,7 +13,7 @@

/// Updates an organisation member's permissions

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 @@

/// Adds a new member to the organisation with a given set of permissions.

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 @@

/// Deletes a member from the organisation entirely

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>,
diff --git a/chartered-web/src/endpoints/web_api/users/heatmap.rs b/chartered-web/src/endpoints/web_api/users/heatmap.rs
index c97ac4a..60fcebf 100644
--- a/chartered-web/src/endpoints/web_api/users/heatmap.rs
+++ a/chartered-web/src/endpoints/web_api/users/heatmap.rs
@@ -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)
diff --git a/chartered-web/src/endpoints/web_api/users/info.rs b/chartered-web/src/endpoints/web_api/users/info.rs
index 9fd6658..36bfbaf 100644
--- a/chartered-web/src/endpoints/web_api/users/info.rs
+++ a/chartered-web/src/endpoints/web_api/users/info.rs
@@ -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)?;
diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte
index f4e5ba0..5f44422 100644
--- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte
+++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte
@@ -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();
diff --git a/chartered-frontend/src/routes/(authed)/organisations/create/+page.svelte b/chartered-frontend/src/routes/(authed)/organisations/create/+page.svelte
index 67d0020..59a303d 100644
--- a/chartered-frontend/src/routes/(authed)/organisations/create/+page.svelte
+++ a/chartered-frontend/src/routes/(authed)/organisations/create/+page.svelte
@@ -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();

diff --git a/chartered-frontend/src/routes/(authed)/sessions/list/DeleteSessionModal.svelte b/chartered-frontend/src/routes/(authed)/sessions/list/DeleteSessionModal.svelte
index a39ca69..36024fa 100644
--- a/chartered-frontend/src/routes/(authed)/sessions/list/DeleteSessionModal.svelte
+++ a/chartered-frontend/src/routes/(authed)/sessions/list/DeleteSessionModal.svelte
@@ -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();