-
Notifications
You must be signed in to change notification settings - Fork 258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(auth): add subscription items endpoint #1478
Changes from 35 commits
d3809f2
2820a16
05574fa
c90bc91
2a24abb
7ea8662
950433d
c4b32d0
4340985
f1ceb4d
c478b66
d2971de
a83666e
b9cbd62
18d9730
c352e7b
a5ddd1b
3a3c287
d38a53f
0028291
e78d42b
e5e653e
a8793c8
130ee9d
099f59d
4a500ae
e508559
1dc1fd3
8eb24d7
6499db2
574a486
4d2b659
44381de
0a081da
76ddf5b
9bbd36e
6835532
4317ee6
39f0897
2029032
89f8b3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# 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`. | ||
|
||
## Running the binary on it's own | ||
|
||
**The below commands are ran from the root of the repo** | ||
|
||
- First, start the control db container: | ||
|
||
``` | ||
docker compose -f docker-compose.rendered.yml up control-db | ||
``` | ||
|
||
- Then insert an admin user into the database: | ||
|
||
``` | ||
cargo run --bin shuttle-auth -- --db-connection-uri postgres://postgres:postgres@localhost:5434/postgres init-admin --name admin | ||
``` | ||
|
||
- Then start the service, you can get a stripe-secret-key from the Stripe dashboard. **Always use the test Stripe API for this**. See instructions above for generating a jwt-signing-private-key. | ||
|
||
``` | ||
cargo run --bin shuttle-auth -- --db-connection-uri postgres://postgres:postgres@localhost:5434/pos | ||
tgres start --stripe-secret-key sk_test_<test key> --jwt-signing-private-key <key> | ||
``` | ||
oddgrd marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,14 +2,18 @@ use std::{net::SocketAddr, sync::Arc}; | |
|
||
use axum::{ | ||
extract::FromRef, | ||
handler::Handler, | ||
middleware::from_extractor, | ||
routing::{get, post, put}, | ||
Router, Server, | ||
}; | ||
use axum_sessions::{async_session::MemoryStore, SessionLayer}; | ||
use rand::RngCore; | ||
use shuttle_common::{ | ||
backends::metrics::{Metrics, TraceLayer}, | ||
backends::{ | ||
auth::JwtAuthenticationLayer, | ||
metrics::{Metrics, TraceLayer}, | ||
}, | ||
request_span, | ||
}; | ||
use sqlx::PgPool; | ||
|
@@ -22,8 +26,8 @@ use crate::{ | |
}; | ||
|
||
use super::handlers::{ | ||
convert_cookie, convert_key, get_public_key, get_user, health_check, logout, post_user, | ||
put_user_reset_key, refresh_token, update_user_tier, | ||
add_subscription_items, convert_cookie, convert_key, get_public_key, get_user, health_check, | ||
logout, post_user, put_user_reset_key, refresh_token, update_user_tier, | ||
}; | ||
|
||
pub type UserManagerState = Arc<Box<dyn UserManagement>>; | ||
|
@@ -33,6 +37,7 @@ pub type KeyManagerState = Arc<Box<dyn KeyManager>>; | |
pub struct RouterState { | ||
pub user_manager: UserManagerState, | ||
pub key_manager: KeyManagerState, | ||
pub rds_price_id: String, | ||
} | ||
|
||
// Allow getting a user management state directly | ||
|
@@ -54,17 +59,16 @@ pub struct ApiBuilder { | |
pool: Option<PgPool>, | ||
session_layer: Option<SessionLayer<MemoryStore>>, | ||
stripe_client: Option<stripe::Client>, | ||
jwt_signing_private_key: Option<String>, | ||
} | ||
|
||
impl Default for ApiBuilder { | ||
fn default() -> Self { | ||
Self::new() | ||
} | ||
rds_price_id: Option<String>, | ||
key_manager: EdDsaManager, | ||
} | ||
|
||
impl ApiBuilder { | ||
pub fn new() -> Self { | ||
pub fn new(jwt_signing_private_key: String) -> Self { | ||
let key_manager = EdDsaManager::new(jwt_signing_private_key); | ||
|
||
let public_key = key_manager.public_key().to_vec(); | ||
|
||
let router = Router::new() | ||
.route("/", get(health_check)) | ||
.route("/logout", post(logout)) | ||
|
@@ -73,6 +77,15 @@ impl ApiBuilder { | |
.route("/auth/refresh", post(refresh_token)) | ||
.route("/public-key", get(get_public_key)) | ||
.route("/users/:account_name", get(get_user)) | ||
.route( | ||
"/users/subscription/items", | ||
post( | ||
add_subscription_items.layer(JwtAuthenticationLayer::new(move || { | ||
let public_key = public_key.clone(); | ||
async move { public_key.clone() } | ||
oddgrd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
})), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed only on this endpoint? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The auth API has only dealt with API keys or cookies so far, but rather than creating an admin user for provisioner and using that api key to call auth, we decided to just pass on the JWT of the user. This will be the only endpoint that needs to take a JWT, so we're just putting the layer on this handler for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So what happens if there is no cached JWT and the first request made is to this endpoint? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, what do you mean by that? The provisioner needs a JWT to provision RDS, and it then uses this JWT to call the subscription update. It could be problematic if the JWT expires while provisioning the RDS instance, though, but in that case (which should be rare) we may just delete the RDS instance and the user needs to try again. |
||
), | ||
) | ||
.route( | ||
"/users/:account_name/:account_tier", | ||
post(post_user).put(update_user_tier), | ||
|
@@ -96,7 +109,8 @@ impl ApiBuilder { | |
pool: None, | ||
session_layer: None, | ||
stripe_client: None, | ||
jwt_signing_private_key: None, | ||
rds_price_id: None, | ||
key_manager, | ||
} | ||
} | ||
|
||
|
@@ -124,27 +138,26 @@ impl ApiBuilder { | |
self | ||
} | ||
|
||
pub fn with_jwt_signing_private_key(mut self, private_key: String) -> Self { | ||
self.jwt_signing_private_key = Some(private_key); | ||
pub fn with_rds_price_id(mut self, price_id: String) -> Self { | ||
self.rds_price_id = Some(price_id); | ||
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 rds_price_id = self.rds_price_id.expect("rds price id is required"); | ||
|
||
let user_manager = UserManager { | ||
pool, | ||
stripe_client, | ||
}; | ||
let key_manager = EdDsaManager::new(jwt_signing_private_key); | ||
|
||
let state = RouterState { | ||
user_manager: Arc::new(Box::new(user_manager)), | ||
key_manager: Arc::new(Box::new(key_manager)), | ||
key_manager: Arc::new(Box::new(self.key_manager)), | ||
rds_price_id, | ||
}; | ||
|
||
self.router.layer(session_layer).with_state(state) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reminder: to be removed before merging.