diff --git a/.env b/.env index 5d75d99c..9b2fc986 100644 --- a/.env +++ b/.env @@ -1,4 +1,74 @@ ENV=DEV # PROD or DEV ONLY_RELAY_SERVICE=True # TRUE - This will start only nightly relay service without cloud service NONCE=VERY_SECRET_NONCE -JWT_SECRET=VERY_SECRET_SECRET +# Generated so it can work with grafana +# ssh-keygen -t rsa -b 4096 -m PEM -f grafana.key -N "" +# openssl rsa -in grafana.key -pubout -outform PEM -out grafana.key.pub +# TEST KEY DO NO USE IN PRODUCTION +JWT_SECRET="-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- +" +JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzlUYIpqSUAyLJaf8ZUef +06YBh5DcmaTrwGcVwC57VtywY7bHXQUtGooULQjiYgnyOPxHDt2W+gQW1axiMxOQ +ew0MF0kXYQhg0+WA1dcWOsxCDpyLc4z+E7EmdV7MTuvsDR2TLhZ6DFmVbN4ca64W +vftz//3Ptc/I1/VpfGsHWb0VpmphkbE9vWGHzzJVp/Lvtk7ybcpfxiiNWEi7unr7 +/TIEsqy93lCRfpLKIvO06ITH1GZz7+VPL/8q8lrH5kSwxqo5szq1mEa0pNN6hTpo +cz00x5oBfL9K/TNLQNgdb5uOOfjQ9pYtZ49UDyMLs5lCsZ6ue+SIbWrJXDEPMIEg +Lzx9nnvytL9wbiwo5GQdIaDAZXMwVVdKQGyqx+UhaHmrb1u0RVRcFzO/QPWzNCWv +oxJGfg9dVTxfjaa1HzQ0irPs86zCQUnt0OyTuq0SmUgTCNXLSQaN8MigN++zwT4c +IRDB0WMgmu8sxtnmA3Zq+2GYT97jmbdVMB34DVbXI4UigD3UieVe4rO95cEssoUT +AFG0pthv64BRKIIRNch+eiBBjkGpnWKRybzsmqnEqpMf9v7KQU9hmpBJDbi6KA6w +3fg3KSroJZwosbAwdBuylCfQgq1jZ6UfR/ss9kYGhaWbCB3ARmY9XHFc+OZuFW0p +ohgfBfs9RDMDVkTa4tzBAGsCAwEAAQ== +-----END PUBLIC KEY----- +" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 07494c29..adaa3b8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ ctrlc = "3.4.2" serde = { version = "1.0.197", features = ["derive"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } once_cell = "1.19.0" -garde = { version = "0.18.0", features = ["derive", "email", "regex"]} +garde = { version = "0.18.0", features = ["derive", "email", "regex"] } anyhow = "1.0.79" serde_json = "1.0.113" strum = { version = "0.26.1", features = ["derive"] } @@ -32,5 +32,11 @@ tokio = { version = "1.35.1", features = ["full"] } async-trait = "0.1.77" # If you're updating sqlx, make sure that chrono version below is the same as the one in sqlx -sqlx = { version = "0.7.3", features = [ "runtime-tokio", "tls-rustls", "macros", "postgres", "chrono"] } +sqlx = { version = "0.7.3", features = [ + "runtime-tokio", + "tls-rustls", + "macros", + "postgres", + "chrono", +] } chrono = { version = "0.4.22", features = ["serde"] } diff --git a/grafana/.gitignore b/grafana/.gitignore new file mode 100644 index 00000000..37ce6dee --- /dev/null +++ b/grafana/.gitignore @@ -0,0 +1 @@ +grafana-data \ No newline at end of file diff --git a/grafana/docker-compose.yml b/grafana/docker-compose.yml new file mode 100644 index 00000000..2d8caf65 --- /dev/null +++ b/grafana/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.1' + +services: + grafana: + image: grafana/grafana:latest + # container_name: grafana + # environment: + # - GF_SECURITY_ADMIN_PASSWORD=secret # Change 'secret' to a strong password + # - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - ./grafana-data:/var/lib/grafana # Persists Grafana data + - ./grafana.ini:/etc/grafana/grafana.ini # Optional: Custom Grafana configuration + - ../jwt_keys/grafana.key.pub:/etc/grafana/public-key.pem # Optional: Custom Grafana configuration + ports: + - '3000:3000' + restart: unless-stopped + +volumes: + grafana-data: diff --git a/grafana/grafana.ini b/grafana/grafana.ini new file mode 100644 index 00000000..da7a7388 --- /dev/null +++ b/grafana/grafana.ini @@ -0,0 +1,8 @@ +[auth.jwt] +enabled = true +header_name = X-JWT-Assertion +email_claim = sub +username_claim = sub +auto_sign_up = true +key_file = /etc/grafana/public-key.pem +url_login = true \ No newline at end of file diff --git a/grafana/readme.md b/grafana/readme.md new file mode 100644 index 00000000..e7dbd44d --- /dev/null +++ b/grafana/readme.md @@ -0,0 +1,6 @@ +// Setup + +1. Use openresty +2. Use grafana without auth +3. Generate JWT on backed +4. Validate JWT in openresty/nginx diff --git a/jwt_keys/grafana.key b/jwt_keys/grafana.key new file mode 100644 index 00000000..0575cd21 --- /dev/null +++ b/jwt_keys/grafana.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/jwt_keys/grafana.key.pub b/jwt_keys/grafana.key.pub new file mode 100644 index 00000000..eb26dc30 --- /dev/null +++ b/jwt_keys/grafana.key.pub @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzlUYIpqSUAyLJaf8ZUef +06YBh5DcmaTrwGcVwC57VtywY7bHXQUtGooULQjiYgnyOPxHDt2W+gQW1axiMxOQ +ew0MF0kXYQhg0+WA1dcWOsxCDpyLc4z+E7EmdV7MTuvsDR2TLhZ6DFmVbN4ca64W +vftz//3Ptc/I1/VpfGsHWb0VpmphkbE9vWGHzzJVp/Lvtk7ybcpfxiiNWEi7unr7 +/TIEsqy93lCRfpLKIvO06ITH1GZz7+VPL/8q8lrH5kSwxqo5szq1mEa0pNN6hTpo +cz00x5oBfL9K/TNLQNgdb5uOOfjQ9pYtZ49UDyMLs5lCsZ6ue+SIbWrJXDEPMIEg +Lzx9nnvytL9wbiwo5GQdIaDAZXMwVVdKQGyqx+UhaHmrb1u0RVRcFzO/QPWzNCWv +oxJGfg9dVTxfjaa1HzQ0irPs86zCQUnt0OyTuq0SmUgTCNXLSQaN8MigN++zwT4c +IRDB0WMgmu8sxtnmA3Zq+2GYT97jmbdVMB34DVbXI4UigD3UieVe4rO95cEssoUT +AFG0pthv64BRKIIRNch+eiBBjkGpnWKRybzsmqnEqpMf9v7KQU9hmpBJDbi6KA6w +3fg3KSroJZwosbAwdBuylCfQgq1jZ6UfR/ss9kYGhaWbCB3ARmY9XHFc+OZuFW0p +ohgfBfs9RDMDVkTa4tzBAGsCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/jwt_keys/test.py b/jwt_keys/test.py new file mode 100644 index 00000000..569c77c9 --- /dev/null +++ b/jwt_keys/test.py @@ -0,0 +1,17 @@ +import jwt +import time +import webbrowser + +private_key = open('./grafana.key', 'r').read() + +payload = { + "sub": "yolo2", + "iat": int(time.time()) +} + +token = jwt.encode(payload, private_key, algorithm='RS256') + +base_url = 'http://localhost:3000' +url_with_token = f'{base_url}?auth_token={token}' +print(url_with_token) +# webbrowser.open_new_tab(url_with_token) \ No newline at end of file diff --git a/server/bindings/CloudApiErrors.ts b/server/bindings/CloudApiErrors.ts index 330fdb2f..043cb0cf 100644 --- a/server/bindings/CloudApiErrors.ts +++ b/server/bindings/CloudApiErrors.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CloudApiErrors = "TeamDoesNotExist" | "UserDoesNotExist" | "CloudFeatureDisabled" | "InsufficientPermissions" | "TeamHasNoRegisteredApps" | "DatabaseError" | "MaximumUsersPerTeamReached" | "UserAlreadyBelongsToTheTeam" | "IncorrectPassword" | "AccessTokenFailure" | "RefreshTokenFailure" | "AppAlreadyExists" | "MaximumAppsPerTeamReached" | "TeamAlreadyExists" | "PersonalTeamAlreadyExists" | "EmailAlreadyExists" | "InternalServerError" | "UserDoesNotBelongsToTheTeam"; \ No newline at end of file +export type CloudApiErrors = "TeamDoesNotExist" | "UserDoesNotExist" | "CloudFeatureDisabled" | "InsufficientPermissions" | "TeamHasNoRegisteredApps" | "DatabaseError" | "MaximumUsersPerTeamReached" | "UserAlreadyBelongsToTheTeam" | "IncorrectPassword" | "AccessTokenFailure" | "RefreshTokenFailure" | "AppAlreadyExists" | "MaximumAppsPerTeamReached" | "TeamAlreadyExists" | "PersonalTeamAlreadyExists" | "EmailAlreadyExists" | "InternalServerError" | "UserDoesNotBelongsToTheTeam" | "InvalidName"; \ No newline at end of file diff --git a/server/src/auth/auth_middleware.rs b/server/src/auth/auth_middleware.rs index 20f7e4c7..35fcd6f9 100644 --- a/server/src/auth/auth_middleware.rs +++ b/server/src/auth/auth_middleware.rs @@ -1,5 +1,5 @@ use super::{auth_token_type::AuthTokenType, AuthToken}; -use crate::env::JWT_SECRET; +use crate::env::JWT_PUBLIC_KEY; use axum::{ extract::{ConnectInfo, Request}, http::{HeaderMap, StatusCode}, @@ -27,7 +27,7 @@ pub async fn access_auth_middleware( Some(auth_header) => { let token = auth_header.replace("Bearer ", ""); // Decode and validate the token - let decoded = match AuthToken::decode(&token, JWT_SECRET(), ip) { + let decoded = match AuthToken::decode(&token, JWT_PUBLIC_KEY(), ip) { Ok(decoded) => decoded, Err(e) => return Err((StatusCode::UNAUTHORIZED, e.to_string())), }; diff --git a/server/src/auth/auth_token.rs b/server/src/auth/auth_token.rs index e49c4b8b..082ef34e 100644 --- a/server/src/auth/auth_token.rs +++ b/server/src/auth/auth_token.rs @@ -10,6 +10,7 @@ pub struct AuthToken { pub user_id: String, pub ip: Option, pub token_type: AuthTokenType, + pub sub: String, pub exp: u64, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp) } @@ -23,6 +24,7 @@ impl AuthToken { None => None, }, token_type: AuthTokenType::Access, + sub: user_id.clone(), exp: (Utc::now() + Duration::minutes(30)).timestamp() as u64, // Token expires in 30 minutes } } @@ -35,48 +37,45 @@ impl AuthToken { None => None, }, token_type: AuthTokenType::Refresh, + sub: user_id.clone(), exp: (Utc::now() + Duration::minutes(60 * 7 * 24)).timestamp() as u64, // Token expires in 7 days } } pub fn encode(&self, secret: &str) -> Result { - encode( - &Header::new(Algorithm::HS256), - &self, - &EncodingKey::from_secret(secret.as_ref()), - ) + EncodingKey::from_rsa_pem(secret.as_bytes()) + .map_err(|e| e.into()) + .and_then(|key| encode(&Header::new(Algorithm::RS256), &self, &key)) } + pub fn decode( token: &str, - secret: &str, + public_key: &str, ip: SocketAddr, ) -> Result { - let token_data = match decode::( - token, - &DecodingKey::from_secret(secret.as_ref()), - &Validation::new(Algorithm::HS256), - ) { - Ok(token) => token.claims, - Err(e) => return Err(e), - }; - - match token_data.ip { - Some(token_ip) => { - if token_ip == ip.ip() { - return Ok(token_data); - } else { - return Err(jsonwebtoken::errors::Error::from( - jsonwebtoken::errors::ErrorKind::InvalidToken, - )); - } + let decoding_key = DecodingKey::from_rsa_pem(public_key.as_bytes()).map_err(|e| e)?; + + let token_data = + decode::(token, &decoding_key, &Validation::new(Algorithm::RS256))?.claims; + + if let Some(token_ip) = token_data.ip { + if token_ip == ip.ip() { + Ok(token_data) + } else { + Err(jsonwebtoken::errors::Error::from( + jsonwebtoken::errors::ErrorKind::InvalidToken, + )) } + } else { // Token does not have to be ip specific - None => return Ok(token_data), + Ok(token_data) } } } #[cfg(test)] mod tests { + use crate::env::{JWT_PUBLIC_KEY, JWT_SECRET}; + use super::*; #[test] @@ -84,7 +83,6 @@ mod tests { // Test the `new` method to create a new `AuthToken` instance. let ip = SocketAddr::from(([123, 233, 3, 21], 8080)); let auth_token = AuthToken::new_access(&"1".to_string(), Some(ip)); - // Check that the `user_id` and `exp` fields are set correctly. assert_eq!(auth_token.user_id, "1".to_string()); assert_eq!(auth_token.ip.unwrap(), ip.ip()); @@ -96,8 +94,7 @@ mod tests { // Test the `encode` method to generate a JWT from an `AuthToken` instance. let ip = SocketAddr::from(([123, 233, 3, 21], 8080)); let auth_token = AuthToken::new_access(&"1".to_string(), Some(ip)); - let secret = "secret-key"; - let token = auth_token.encode(secret).unwrap(); + let token = auth_token.encode(JWT_SECRET()).unwrap(); // Check that the JWT is a non-empty string. assert!(!token.is_empty()); @@ -108,9 +105,8 @@ mod tests { // Test the `decode` method to parse a JWT and create an `AuthToken` instance. let ip = SocketAddr::from(([123, 233, 3, 21], 8080)); let auth_token = AuthToken::new_access(&"1".to_string(), Some(ip)); - let secret = "secret-key"; - let token = auth_token.encode(secret).unwrap(); - let decoded_auth_token = AuthToken::decode(&token, secret, ip).unwrap(); + let token = auth_token.encode(JWT_SECRET()).unwrap(); + let decoded_auth_token = AuthToken::decode(&token, JWT_PUBLIC_KEY(), ip).unwrap(); // Check that the decoded `AuthToken` instance has the same `user_id` and `exp` fields as the original. assert_eq!(decoded_auth_token.ip, auth_token.ip); @@ -120,12 +116,11 @@ mod tests { #[test] fn test_auth_token_decode_invalid_token() { // Test the `decode` method with an invalid JWT. - let secret = "secret-key"; let token = "invalid-token"; let ip = SocketAddr::from(([123, 233, 3, 21], 8080)); // Call the `decode` method and use a `match` expression to handle the error case explicitly. - let result = AuthToken::decode(token, secret, ip); + let result = AuthToken::decode(token, JWT_SECRET(), ip); match result { Ok(_) => panic!("Expected an error but got a successful result"), _ => (), @@ -134,16 +129,29 @@ mod tests { #[test] fn test_auth_token_decode_incorrect_secret() { - // Test the `decode` method with the incorrect secret key. + // Test the `decode` method with the incorrect public key. let ip = SocketAddr::from(([123, 233, 3, 21], 8080)); let auth_token = AuthToken::new_access(&"1".to_string(), Some(ip)); - let correct_secret = "secret-key"; - let incorrect_secret = "incorrect-key"; - - let token = auth_token.encode(correct_secret).unwrap(); + let incorrect_public_key = "-----BEGIN PUBLIC KEY----- + MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzlUYIpqSUAyLJaf8ZUef + 06YBh5DcmaTrwGcVwC57VtywY7bHXQUtGooULQjiYgnyOPxHDt2W+gQW1axiMxOQ + ew0MF0kXYQhg0+WA1dcWOsxCDpyLc4z+E7EmdV7MTuvsDR2TLhZ6DFmVbN4ca64W + vftz//3Ptc/I1/VpfGsHWb0VpmphkbE9vWGHzzJVp/Lvtk7ybcpfxiiNWEi7unr7 + /TIEsqy93lCRfpLKIvO06ITH1GZz7+VPL/8q8lrH5kSwxqo5szq1mEa0pNN6hTpo + cz00x5oBfL9K/TNLQNgdb5uOOfjQ9pYtZ49UDyMLs5lCsZ6ue+SIbWrJXDEPMIEg + Lzx9nnvytL9wbiwo5GQdIaDAZXMwVVdKQGyqx+UhaHmrb1u0RVRcFzO/QPWzNCWv + oxJGfg9dVTxfjaa1HzQ0irPs86zCQUnt0OyTuq0SmUgTCNXLSQaN8MigN++zwT4c + IRDB0WMgmu8sxtnmA3Zq+2GYT97jmbdVMB34DVbXI4UigD3UieVe4rO95cEssoUT + AFG0pthv64BRKIIRNch+eiBBjkGpnWCRybzsmqnEqpMf9v7KQU9hmpBJDbi6KA6w + 3fg3KSroJZwosbAwdBuylCfQgq1jZ6UfR/ss9kYGhaWbCB3ARmY9XHFc+OZuFW0p + ohgfBfs9RDMDVkTa4tzBAGsCAwEAAQ== + -----END PUBLIC KEY----- + "; + + let token = auth_token.encode(JWT_SECRET()).unwrap(); // Call the `decode` method and use a `match` expression to handle the error case explicitly. - let result = AuthToken::decode(&token, incorrect_secret, ip); + let result = AuthToken::decode(&token, incorrect_public_key, ip); match result { Ok(_) => panic!("Expected an error but got a successful result"), _ => (), @@ -152,23 +160,23 @@ mod tests { #[test] fn test_auth_token_decode_expired_token() { // Test the `decode` method with an expired JWT. - let secret = "secret-key"; let user_id = "1".to_string(); let ip = SocketAddr::from(([123, 233, 3, 21], 8080)); // Set the expiration time of the `AuthToken` instance to a time in the past. let exp = (Utc::now() - Duration::minutes(30)).timestamp() as u64; let auth_token = AuthToken { id: uuid7::uuid7().to_string(), - user_id, + user_id: user_id.clone(), exp, ip: Some(ip.ip()), token_type: AuthTokenType::Access, + sub: user_id, }; - let token = auth_token.encode(secret).unwrap(); + let token = auth_token.encode(JWT_SECRET()).unwrap(); // Call the `decode` method and assert that it returns an error. - let result = AuthToken::decode(&token, secret, ip); + let result = AuthToken::decode(&token, JWT_PUBLIC_KEY(), ip); assert!(result.is_err()); } #[test] @@ -176,13 +184,12 @@ mod tests { // Test the `decode` method with an expired JWT. let ip = SocketAddr::from(([123, 233, 3, 21], 8080)); let auth_token = AuthToken::new_access(&"1".to_string(), Some(ip)); - let secret = "secret-key"; - let token = auth_token.encode(secret).unwrap(); + let token = auth_token.encode(JWT_SECRET()).unwrap(); let different_ip = SocketAddr::from(([2, 133, 3, 21], 8080)); - let result = AuthToken::decode(&token, secret, ip); + let result = AuthToken::decode(&token, JWT_PUBLIC_KEY(), ip); assert!(result.is_ok()); - let result = AuthToken::decode(&token, secret, different_ip); + let result = AuthToken::decode(&token, JWT_PUBLIC_KEY(), different_ip); assert!(result.is_err()); } } diff --git a/server/src/env.rs b/server/src/env.rs index 5a6c9a0b..edd9aace 100644 --- a/server/src/env.rs +++ b/server/src/env.rs @@ -1,11 +1,11 @@ #![allow(non_snake_case)] use once_cell::sync::OnceCell; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; #[derive(Debug)] pub struct ENV { pub ENVIRONMENT: String, pub JWT_SECRET: String, + pub JWT_PUBLIC_KEY: String, pub ONLY_RELAY_SERVICE: bool, pub NONCE: String, } @@ -19,20 +19,8 @@ pub fn get_env() -> &'static ENV { let env = ENV { ENVIRONMENT: ENVIRONMENT.to_owned(), - JWT_SECRET: match ENVIRONMENT { - "PROD" => std::env::var("JWT_SECRET").expect("JWT_SECRET env not set"), - "DEV" => { - let rand_string: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(6) - .map(char::from) - .collect(); - - std::env::var("JWT_SECRET").expect("JWT_SECRET env not set") - + rand_string.as_str() - } - _ => panic!("Invalid ENVIRONMENT"), - }, + JWT_SECRET: std::env::var("JWT_SECRET").expect("JWT_SECRET env not set"), + JWT_PUBLIC_KEY: std::env::var("JWT_PUBLIC_KEY").expect("JWT_PUBLIC_KEY env not set"), ONLY_RELAY_SERVICE: std::env::var("ONLY_RELAY_SERVICE") .expect("Failed to get ONLY_RELAY_SERVICE env") .eq_ignore_ascii_case("true"), @@ -49,6 +37,9 @@ pub fn JWT_SECRET() -> &'static str { get_env().JWT_SECRET.as_str() } +pub fn JWT_PUBLIC_KEY() -> &'static str { + get_env().JWT_PUBLIC_KEY.as_str() +} pub fn ONLY_RELAY_SERVICE() -> bool { get_env().ONLY_RELAY_SERVICE } diff --git a/server/src/test_utils.rs b/server/src/test_utils.rs index 576f60fd..6a3a8294 100644 --- a/server/src/test_utils.rs +++ b/server/src/test_utils.rs @@ -2,7 +2,7 @@ pub mod test_utils { use crate::{ auth::AuthToken, - env::JWT_SECRET, + env::{JWT_PUBLIC_KEY, JWT_SECRET}, http::cloud::{ login_with_password::{HttpLoginRequest, HttpLoginResponse}, register_new_team::{HttpRegisterNewTeamRequest, HttpRegisterNewTeamResponse}, @@ -145,7 +145,7 @@ pub mod test_utils { ) .unwrap(); - let auth_token = AuthToken::decode(&response.auth_token, JWT_SECRET(), ip.0).unwrap(); + let auth_token = AuthToken::decode(&response.auth_token, JWT_PUBLIC_KEY(), ip.0).unwrap(); return (auth_token, email, password); }