Skip to content

Commit fa667a6

Browse files
committed
make auth errors JSON
1 parent c2db060 commit fa667a6

3 files changed

Lines changed: 66 additions & 24 deletions

File tree

src/auth.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
use axum::{
2-
body::Body,
3-
extract::State,
4-
http::{Request, StatusCode},
5-
middleware::Next,
6-
response::Response,
7-
};
1+
use axum::{body::Body, extract::State, http::Request, middleware::Next, response::Response};
82
use biscuit_auth::{macros::authorizer, Biscuit, PublicKey};
93
use std::{
104
collections::HashSet,
@@ -16,7 +10,7 @@ use std::{
1610
use tempfile::NamedTempFile;
1711

1812
use crate::{
19-
error::{APIError, AppError},
13+
error::{APIError, AppError, AuthError},
2014
utils::{hex_str, hex_str_to_vec, AppState},
2115
};
2216

@@ -88,7 +82,7 @@ pub(crate) async fn conditional_auth_middleware(
8882
State(app_state): State<Arc<AppState>>,
8983
request: Request<Body>,
9084
next: Next,
91-
) -> Result<Response, StatusCode> {
85+
) -> Result<Response, AuthError> {
9286
let Some(root_pubkey) = app_state.root_public_key else {
9387
// if no root key is configured, skip authentication
9488
return Ok(next.run(request).await);
@@ -101,19 +95,19 @@ pub(crate) async fn conditional_auth_middleware(
10195
.and_then(|s| s.strip_prefix("Bearer "));
10296
let auth_token = match auth_header {
10397
Some(token) => token,
104-
None => return Err(StatusCode::UNAUTHORIZED),
98+
None => return Err(AuthError::Unauthorized),
10599
};
106100

107101
// verify the token
108102
let token =
109-
Biscuit::from_base64(auth_token, root_pubkey).map_err(|_| StatusCode::UNAUTHORIZED)?;
103+
Biscuit::from_base64(auth_token, root_pubkey).map_err(|_| AuthError::Unauthorized)?;
110104

111105
if app_state.is_token_revoked(&token) {
112-
return Err(StatusCode::UNAUTHORIZED);
106+
return Err(AuthError::Unauthorized);
113107
}
114108

115109
if is_token_expired(&token) {
116-
return Err(StatusCode::UNAUTHORIZED);
110+
return Err(AuthError::Unauthorized);
117111
}
118112

119113
if is_admin_role(&token) {
@@ -126,19 +120,19 @@ pub(crate) async fn conditional_auth_middleware(
126120
if is_operation_readonly(&op) {
127121
return Ok(next.run(request).await);
128122
} else {
129-
return Err(StatusCode::FORBIDDEN);
123+
return Err(AuthError::Forbidden);
130124
}
131125
}
132126

133127
if is_custom_role(&token) {
134128
if is_operation_permitted(&token, &op) {
135129
return Ok(next.run(request).await);
136130
} else {
137-
return Err(StatusCode::FORBIDDEN);
131+
return Err(AuthError::Forbidden);
138132
}
139133
}
140134

141-
Err(StatusCode::UNAUTHORIZED)
135+
Err(AuthError::Unauthorized)
142136
}
143137

144138
fn is_admin_role(token: &Biscuit) -> bool {

src/error.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,3 +548,35 @@ pub enum AppError {
548548
#[error("Port {0} is unavailable")]
549549
UnavailablePort(u16),
550550
}
551+
552+
/// The error variants returned by the authentication checks
553+
#[derive(Debug)]
554+
pub enum AuthError {
555+
Unauthorized,
556+
Forbidden,
557+
}
558+
559+
impl IntoResponse for AuthError {
560+
fn into_response(self) -> Response {
561+
match self {
562+
AuthError::Unauthorized => (
563+
StatusCode::UNAUTHORIZED,
564+
Json(APIErrorResponse {
565+
code: StatusCode::UNAUTHORIZED.as_u16(),
566+
error: s!("Missing or invalid credentials"),
567+
name: s!("Unauthorized"),
568+
}),
569+
)
570+
.into_response(),
571+
AuthError::Forbidden => (
572+
StatusCode::FORBIDDEN,
573+
Json(APIErrorResponse {
574+
code: StatusCode::FORBIDDEN.as_u16(),
575+
error: s!("You don't have access to this resource"),
576+
name: s!("Forbidden"),
577+
}),
578+
)
579+
.into_response(),
580+
}
581+
}
582+
}

src/test/authentication.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ use super::*;
22

33
const TEST_DIR_BASE: &str = "tmp/authentication/";
44

5+
async fn check_forbidden(res: reqwest::Response) {
6+
assert_eq!(res.status(), reqwest::StatusCode::FORBIDDEN);
7+
let body: APIErrorResponse = res.json().await.unwrap();
8+
assert_eq!(body.code, 403);
9+
assert_eq!(body.error, "You don't have access to this resource");
10+
assert_eq!(body.name, "Forbidden");
11+
}
12+
13+
async fn check_unauthorized(res: reqwest::Response) {
14+
assert_eq!(res.status(), reqwest::StatusCode::UNAUTHORIZED);
15+
let body: APIErrorResponse = res.json().await.unwrap();
16+
assert_eq!(body.code, 401);
17+
assert_eq!(body.error, "Missing or invalid credentials");
18+
assert_eq!(body.name, "Unauthorized");
19+
}
20+
521
fn create_token(
622
root: &KeyPair,
723
user_role: Option<&str>,
@@ -105,7 +121,7 @@ async fn authentication() {
105121
.send()
106122
.await
107123
.unwrap();
108-
assert_eq!(res.status(), reqwest::StatusCode::FORBIDDEN);
124+
check_forbidden(res).await;
109125
while Utc::now() < ten_seconds_later {
110126
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
111127
}
@@ -115,7 +131,7 @@ async fn authentication() {
115131
.send()
116132
.await
117133
.unwrap();
118-
assert_eq!(res.status(), reqwest::StatusCode::UNAUTHORIZED);
134+
check_unauthorized(res).await;
119135

120136
// user with no role cannot do any operation
121137
let user_token = create_token(&root_keypair, None, vec!["/nodeinfo"], None);
@@ -125,7 +141,7 @@ async fn authentication() {
125141
.send()
126142
.await
127143
.unwrap();
128-
assert_eq!(res.status(), reqwest::StatusCode::UNAUTHORIZED);
144+
check_unauthorized(res).await;
129145

130146
// user with unknown role cannot do any operation
131147
let user_token = create_token(&root_keypair, Some("unknown"), vec!["/nodeinfo"], None);
@@ -135,7 +151,7 @@ async fn authentication() {
135151
.send()
136152
.await
137153
.unwrap();
138-
assert_eq!(res.status(), reqwest::StatusCode::UNAUTHORIZED);
154+
check_unauthorized(res).await;
139155

140156
// user with read-only role can only call read-only APIs
141157
let user_token = create_token(&root_keypair, Some("read-only"), vec![], None);
@@ -156,7 +172,7 @@ async fn authentication() {
156172
.send()
157173
.await
158174
.unwrap();
159-
assert_eq!(res.status(), reqwest::StatusCode::FORBIDDEN);
175+
check_forbidden(res).await;
160176

161177
// user cannot call any API after token revocation
162178
let user_token = create_token(&root_keypair, Some("custom"), vec!["/nodeinfo"], None);
@@ -192,15 +208,15 @@ async fn authentication() {
192208
.send()
193209
.await
194210
.unwrap();
195-
assert_eq!(res.status(), reqwest::StatusCode::UNAUTHORIZED);
211+
check_unauthorized(res).await;
196212

197213
// with no token no API can be called
198214
let res = reqwest::Client::new()
199215
.get(format!("http://{node_address}/nodeinfo"))
200216
.send()
201217
.await
202218
.unwrap();
203-
assert_eq!(res.status(), reqwest::StatusCode::UNAUTHORIZED);
219+
check_unauthorized(res).await;
204220

205221
// with an invalid token no API can be called
206222
let res = reqwest::Client::new()
@@ -209,5 +225,5 @@ async fn authentication() {
209225
.send()
210226
.await
211227
.unwrap();
212-
assert_eq!(res.status(), reqwest::StatusCode::UNAUTHORIZED);
228+
check_unauthorized(res).await;
213229
}

0 commit comments

Comments
 (0)