Skip to content

Commit

Permalink
auth: use a centrally stored jwt signing private key (#1402)
Browse files Browse the repository at this point in the history
* auth: add JWT signing private key arg

* auth: added jwt signing private key flag to docker-compose

* tests(auth): fix by starting auth server with jwt secret key

* auth/circleci: add jwt signing key env variables for deploy

* auth: added instructions about JWT signing private key generation

* auth: consume AUTH_JWTSIGNING_PRIVATE_KEY from environment

* fix(common): type conversion from str for a custom resource

* auth: replaced deprecated base64::decode call

* ci: comment main filter for staging release

* Revert "auth: consume AUTH_JWTSIGNING_PRIVATE_KEY from environment"

This reverts commit 3f766d3.

* Revert "ci: comment main filter for staging release"

This reverts commit 220df0d.

* nit: simplify comment

Co-authored-by: Pieter <[email protected]>

---------

Co-authored-by: Pieter <[email protected]>
  • Loading branch information
iulianbarbu and chesedo authored Nov 23, 2023
1 parent f37a0e8 commit b7471ac
Show file tree
Hide file tree
Showing 12 changed files with 81 additions and 10 deletions.
6 changes: 6 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,9 @@ jobs:
stripe-secret-key:
description: "Stripe secret key used to connect a client to Stripe backend"
type: string
jwt-signing-private-key:
description: "Auth private key used for JWT signing"
type: string
production:
description: "Push and deploy to production"
type: boolean
Expand Down Expand Up @@ -450,6 +453,7 @@ jobs:
DEPLOYS_API_KEY=${<< parameters.deploys-api-key >>} \
LOGGER_POSTGRES_URI=${<< parameters.logger-postgres-uri >>} \
STRIPE_SECRET_KEY=${<< parameters.stripe-secret-key >>} \
AUTH_JWTSIGNING_PRIVATE_KEY=${<< parameters.jwt-signing-private-key >>} \
make deploy
- when:
condition: << parameters.production >>
Expand Down Expand Up @@ -817,6 +821,7 @@ workflows:
deploys-api-key: DEV_DEPLOYS_API_KEY
logger-postgres-uri: DEV_LOGGER_POSTGRES_URI
stripe-secret-key: DEV_STRIPE_SECRET_KEY
jwt-signing-private-key: DEV_AUTH_JWTSIGNING_PRIVATE_KEY
requires:
- build-and-push-unstable
release:
Expand Down Expand Up @@ -895,6 +900,7 @@ workflows:
deploys-api-key: PROD_DEPLOYS_API_KEY
logger-postgres-uri: PROD_LOGGER_POSTGRES_URI
stripe-secret-key: PROD_STRIPE_SECRET_KEY
jwt-signing-private-key: PROD_AUTH_JWTSIGNING_PRIVATE_KEY
ssh-fingerprint: 6a:c5:33:fe:5b:c9:06:df:99:64:ca:17:0d:32:18:2e
ssh-config-script: production-ssh-config.sh
ssh-host: shuttle.prod.internal
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ POSTGRES_PASSWORD?=postgres
MONGO_INITDB_ROOT_USERNAME?=mongodb
MONGO_INITDB_ROOT_PASSWORD?=password
STRIPE_SECRET_KEY?=""

AUTH_JWTSIGNING_PRIVATE_KEY?=""

ifeq ($(PROD),true)
DOCKER_COMPOSE_FILES=docker-compose.yml
Expand Down Expand Up @@ -137,6 +137,7 @@ DOCKER_COMPOSE_ENV=\
MONGO_INITDB_ROOT_USERNAME=$(MONGO_INITDB_ROOT_USERNAME)\
MONGO_INITDB_ROOT_PASSWORD=$(MONGO_INITDB_ROOT_PASSWORD)\
STRIPE_SECRET_KEY=$(STRIPE_SECRET_KEY)\
AUTH_JWTSIGNING_PRIVATE_KEY=$(AUTH_JWTSIGNING_PRIVATE_KEY)\
DD_ENV=$(DD_ENV)\
USE_TLS=$(USE_TLS)\
COMPOSE_PROFILES=$(COMPOSE_PROFILES)\
Expand Down
2 changes: 2 additions & 0 deletions auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ async-stripe = { version = "0.25.1", default-features = false, features = ["chec
async-trait = { workspace = true }
axum = { workspace = true, features = ["headers"] }
axum-sessions = { workspace = true }
base64 = { workspace = true }
clap = { workspace = true }
http = { workspace = true }
jsonwebtoken = { workspace = true }
opentelemetry = { workspace = true }
pem = "2"
rand = { workspace = true }
ring = { workspace = true }
serde = { workspace = true, features = ["derive"] }
Expand Down
13 changes: 13 additions & 0 deletions auth/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Auth service considerations

## JWT signing private key

Starting the service locally requires provisioning of a base64 encoded PEM encoded PKCS#8 v1 unencrypted private key.
The service was tested with keys generated as follows:

```bash
openssl genpkey -algorithm ED25519 -out auth_jwtsigning_private_key.pem
base64 < auth_jwtsigning_private_key.pem
```

Used `OpenSSL 3.1.2 1 Aug 2023 (Library: OpenSSL 3.1.2 1 Aug 2023)` and `FreeBSD base64`, on a `macOS Sonoma 14.1.1`.
12 changes: 11 additions & 1 deletion auth/src/api/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub struct ApiBuilder {
pool: Option<SqlitePool>,
session_layer: Option<SessionLayer<MemoryStore>>,
stripe_client: Option<stripe::Client>,
jwt_signing_private_key: Option<String>,
}

impl Default for ApiBuilder {
Expand Down Expand Up @@ -95,6 +96,7 @@ impl ApiBuilder {
pool: None,
session_layer: None,
stripe_client: None,
jwt_signing_private_key: None,
}
}

Expand Down Expand Up @@ -122,15 +124,23 @@ impl ApiBuilder {
self
}

pub fn with_jwt_signing_private_key(mut self, private_key: String) -> Self {
self.jwt_signing_private_key = Some(private_key);
self
}

pub fn into_router(self) -> Router {
let pool = self.pool.expect("an sqlite pool is required");
let session_layer = self.session_layer.expect("a session layer is required");
let stripe_client = self.stripe_client.expect("a stripe client is required");
let jwt_signing_private_key = self
.jwt_signing_private_key
.expect("a jwt signing private key");
let user_manager = UserManager {
pool,
stripe_client,
};
let key_manager = EdDsaManager::new();
let key_manager = EdDsaManager::new(jwt_signing_private_key);

let state = RouterState {
user_manager: Arc::new(Box::new(user_manager)),
Expand Down
5 changes: 5 additions & 0 deletions auth/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ pub struct StartArgs {
/// Stripe client secret key
#[arg(long, default_value = "")]
pub stripe_secret_key: String,

/// Auth JWT signing private key, as a base64 encoding of
/// a PEM encoded PKCS#8 v1 formatted unencrypted private key.
#[arg(long, default_value = "")]
pub jwt_signing_private_key: String,
}

#[derive(clap::Args, Debug, Clone)]
Expand Down
1 change: 1 addition & 0 deletions auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub async fn start(pool: SqlitePool, args: StartArgs) -> io::Result<()> {
.with_sqlite_pool(pool)
.with_sessions()
.with_stripe_client(stripe::Client::new(args.stripe_secret_key))
.with_jwt_signing_private_key(args.jwt_signing_private_key)
.into_router();

info!(address=%args.address, "Binding to and listening at address");
Expand Down
28 changes: 20 additions & 8 deletions auth/src/secrets.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use base64::{engine::general_purpose, Engine};
use jsonwebtoken::EncodingKey;
use ring::signature::{Ed25519KeyPair, KeyPair};

Expand All @@ -15,16 +16,27 @@ pub struct EdDsaManager {
}

impl EdDsaManager {
pub fn new() -> Self {
let doc = Ed25519KeyPair::generate_pkcs8(&ring::rand::SystemRandom::new())
.expect("to create a PKCS8 for edDSA");
let encoding_key = EncodingKey::from_ed_der(doc.as_ref());
let pair = Ed25519KeyPair::from_pkcs8(doc.as_ref()).expect("to create a key pair");
let public_key = pair.public_key();
/// Create a new manager from a base64 PEM encoded private key. This key can be generated using:
/// ```bash
/// openssl genpkey -algorithm ED25519 -out auth_jwtsigning_private_key.pem
/// base64 < auth_jwtsigning_private_key.pem
/// ```
pub fn new(jwt_signing_private_key: String) -> Self {
// Decode the base64 encoding.
let pk_bytes = general_purpose::STANDARD
.decode(jwt_signing_private_key)
.expect("to decode base64 pem encoded private key");

// Parse the pem file and the ed25519 private key contained.
let pem_keypair = pem::parse(pk_bytes.clone()).expect("to parse pem encoded private key");
let ed_keypair = Ed25519KeyPair::from_pkcs8_maybe_unchecked(pem_keypair.contents())
.expect("to get PKCS#8 v1 formatted private key from pem encoded key");

Self {
encoding_key,
public_key: public_key.as_ref().to_vec(),
// Wrap the private key as a jwt encoding key.
encoding_key: EncodingKey::from_ed_pem(pk_bytes.as_slice())
.expect("to get an encoding key from pem encoded ed25519 private key"),
public_key: ed_keypair.public_key().as_ref().to_vec(),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions auth/tests/api/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub(crate) async fn app() -> TestApp {
mocked_stripe_server.uri.to_string().as_str(),
"",
))
.with_jwt_signing_private_key("LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSUR5V0ZFYzhKYm05NnA0ZGNLTEwvQWNvVUVsbUF0MVVKSTU4WTc4d1FpWk4KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=".to_string())
.into_router();

TestApp {
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ services:
>&2 echo "The control DB is available - starting shuttle-auth"
exec /usr/local/bin/shuttle-auth "$${@:0}"
command:
- "--state=/var/lib/shuttle-auth"
- "start"
- "--address=0.0.0.0:8000"
- "--stripe-secret-key=${STRIPE_SECRET_KEY}"
# used only for local development
- "--jwt-signing-private-key=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSUR5V0ZFYzhKYm05NnA0ZGNLTEwvQWNvVUVsbUF0MVVKSTU4WTc4d1FpWk4KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo="
otel-collector:
ports:
- 4317:4317
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ services:
- "start"
- "--address=0.0.0.0:8000"
- "--stripe-secret-key=${STRIPE_SECRET_KEY}"
- "--jwt-signing-private-key=${AUTH_JWTSIGNING_PRIVATE_KEY}"
healthcheck:
test: curl --fail http://localhost:8000/ || exit 1
interval: 1m
Expand Down

0 comments on commit b7471ac

Please sign in to comment.