Skip to content

Commit 6f452dd

Browse files
authored
sessions: JWT (#19)
* jwt token sessions # Conflicts: # crates/embucket-lambda/src/main.rs * Requested changes * clippy * fmt * clippy
1 parent 4585041 commit 6f452dd

19 files changed

Lines changed: 293 additions & 85 deletions

File tree

Cargo.lock

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/api-snowflake-rest-sessions/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ snafu = { workspace = true }
1919
tracing = { workspace = true }
2020
uuid = { workspace = true }
2121
regex = { workspace = true }
22+
jsonwebtoken = { workspace = true }
23+
chrono = { workspace = true }
2224

2325
[lints]
2426
workspace = true

crates/api-snowflake-rest-sessions/src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use axum::{Json, http, response::IntoResponse};
22
use error_stack_trace;
33
use http::header::InvalidHeaderValue;
44
use http::{StatusCode, header::MaxSizeReached};
5+
use jsonwebtoken::errors::Error as JwtError;
56
use serde::{Deserialize, Serialize};
67
use snafu::Location;
78
use snafu::prelude::*;
@@ -33,6 +34,14 @@ pub enum Error {
3334
#[snafu(source)]
3435
error: executor::Error,
3536
},
37+
38+
#[snafu(display("Bad authentication token. {error}"))]
39+
BadAuthToken {
40+
#[snafu(source)]
41+
error: JwtError,
42+
#[snafu(implicit)]
43+
location: Location,
44+
},
3645
}
3746

3847
#[derive(Debug, Serialize, Deserialize)]
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use chrono::offset::Local;
2+
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
3+
use serde::{Deserialize, Serialize};
4+
use time::Duration;
5+
use uuid::Uuid;
6+
7+
#[derive(Serialize, Deserialize)]
8+
#[cfg_attr(test, derive(Debug))]
9+
pub struct Claims {
10+
pub sub: String, // token issued to a particular user
11+
pub iat: i64, // Issued At
12+
pub exp: i64, // Expiration Time
13+
pub session_id: String,
14+
}
15+
16+
#[must_use]
17+
pub fn jwt_claims(username: &str, expiration: Duration) -> Claims {
18+
let now = Local::now();
19+
let iat = now.timestamp();
20+
let exp = now.timestamp() + expiration.whole_seconds();
21+
22+
Claims {
23+
sub: username.to_string(),
24+
iat,
25+
exp,
26+
session_id: Uuid::new_v4().to_string(),
27+
}
28+
}
29+
30+
pub fn get_claims_validate_jwt_token(
31+
token: &str,
32+
jwt_secret: &str,
33+
) -> Result<Claims, jsonwebtoken::errors::Error> {
34+
let mut validation = Validation::default();
35+
validation.leeway = 5;
36+
validation.set_required_spec_claims(&["exp"]);
37+
38+
let decoding_key = DecodingKey::from_secret(jwt_secret.as_bytes());
39+
40+
let decoded = decode::<Claims>(token, &decoding_key, &validation)?;
41+
42+
Ok(decoded.claims)
43+
}
44+
45+
pub fn create_jwt<T>(claims: &T, jwt_secret: &str) -> Result<String, jsonwebtoken::errors::Error>
46+
where
47+
T: Serialize,
48+
{
49+
encode(
50+
&Header::default(),
51+
&claims,
52+
&EncodingKey::from_secret(jwt_secret.as_bytes()),
53+
)
54+
}
55+
56+
#[must_use]
57+
pub fn ensure_jwt_secret_is_valid(jwt_secret: &str) -> Option<String> {
58+
if jwt_secret.is_empty() {
59+
return None;
60+
}
61+
Some(jwt_secret.to_string())
62+
}

crates/api-snowflake-rest-sessions/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod error;
2+
pub mod helpers;
23
pub mod layer;
34
pub mod session;
45

crates/api-snowflake-rest-sessions/src/session.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use crate::error as session_error;
2+
use crate::error::BadAuthTokenSnafu;
3+
use crate::helpers::get_claims_validate_jwt_token;
24
use axum::extract::FromRequestParts;
35
use executor::ExecutionAppState;
46
use executor::service::ExecutionService;
@@ -32,12 +34,16 @@ impl SessionStore {
3234
}
3335
}
3436

37+
pub trait JwtSecret {
38+
fn jwt_secret(&self) -> &str;
39+
}
40+
3541
#[derive(Debug, Clone)]
3642
pub struct DFSessionId(pub String);
3743

3844
impl<S> FromRequestParts<S> for DFSessionId
3945
where
40-
S: Send + Sync + ExecutionAppState,
46+
S: Send + Sync + ExecutionAppState + JwtSecret,
4147
{
4248
type Rejection = session_error::Error;
4349

@@ -47,7 +53,11 @@ where
4753
let execution_svc = state.get_execution_svc();
4854

4955
let (session_id, located_at) = if let Some(token) = extract_token_from_auth(&req.headers) {
50-
(token, "auth header")
56+
let jwt_secret = state.jwt_secret();
57+
let jwt_claims =
58+
get_claims_validate_jwt_token(&token, jwt_secret).context(BadAuthTokenSnafu)?;
59+
60+
(jwt_claims.session_id, "auth header")
5161
} else {
5262
//This is guaranteed by the `propagate_session_cookie`, so we can unwrap
5363
let Self(token) = req.extensions.get::<Self>().unwrap();
@@ -97,7 +107,9 @@ pub fn extract_token_from_auth(headers: &HeaderMap) -> Option<String> {
97107
headers.get("authorization").and_then(|value| {
98108
value.to_str().ok().and_then(|auth| {
99109
#[allow(clippy::unwrap_used)]
100-
let re = Regex::new(r#"Snowflake Token="([a-f0-9\-]+)""#).unwrap();
110+
let re = Regex::new(
111+
r#"Snowflake Token="([A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})""#
112+
).unwrap();
101113
re.captures(auth)
102114
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
103115
})

crates/api-snowflake-rest/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ time = { workspace = true }
5151
uuid = { workspace = true }
5252
tokio = { workspace = true }
5353
cfg-if = { workspace = true }
54+
jsonwebtoken = { workspace = true }
5455

5556
[dev-dependencies]
5657
insta = { workspace = true }

crates/api-snowflake-rest/src/models.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,15 @@ impl From<ColumnInfoModel> for ColumnInfo {
141141
pub struct Auth {
142142
pub demo_user: String,
143143
pub demo_password: String,
144+
pub jwt_secret: String,
145+
}
146+
147+
impl Auth {
148+
#[must_use]
149+
pub fn new(jwt_secret: String) -> Self {
150+
Self {
151+
jwt_secret,
152+
..Self::default()
153+
}
154+
}
144155
}

crates/api-snowflake-rest/src/server/error.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use executor::QueryRecordId;
1010
use executor::error::OperationOn;
1111
use executor::error_code::ErrorCode;
1212
use executor::snowflake_error::Entity;
13+
use jsonwebtoken::errors::Error as JwtError;
1314
use snafu::Location;
1415
use snafu::prelude::*;
1516

@@ -70,6 +71,28 @@ pub enum Error {
7071

7172
#[snafu(transparent)]
7273
Execution { source: executor::Error },
74+
75+
#[snafu(display("JWT secret is not set"))]
76+
NoJwtSecret {
77+
#[snafu(implicit)]
78+
location: Location,
79+
},
80+
81+
#[snafu(display("Failed to create JWT: {error}"))]
82+
CreateJwt {
83+
#[snafu(source)]
84+
error: JwtError,
85+
#[snafu(implicit)]
86+
location: Location,
87+
},
88+
89+
#[snafu(display("Bad authentication token. {error}"))]
90+
BadAuthToken {
91+
#[snafu(source)]
92+
error: JwtError,
93+
#[snafu(implicit)]
94+
location: Location,
95+
},
7396
}
7497

7598
impl IntoResponse for Error {
@@ -190,7 +213,10 @@ impl Error {
190213
}
191214
Self::MissingAuthToken { .. }
192215
| Self::InvalidAuthData { .. }
193-
| Self::InvalidAuthToken { .. } => (
216+
| Self::InvalidAuthToken { .. }
217+
| Self::NoJwtSecret { .. }
218+
| Self::CreateJwt { .. }
219+
| Self::BadAuthToken { .. } => (
194220
http::StatusCode::UNAUTHORIZED,
195221
SqlState::Success,
196222
ErrorCode::Other,

crates/api-snowflake-rest/src/server/helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use executor::utils::{
1414
};
1515
use serde_json::value::RawValue;
1616
use snafu::ResultExt;
17+
use tracing;
1718
use uuid::Uuid;
1819

1920
// https://arrow.apache.org/docs/format/Columnar.html#buffer-alignment-and-padding

0 commit comments

Comments
 (0)