From eba3566225855119572fa840d98cb932cd603799 Mon Sep 17 00:00:00 2001 From: Ethan Donowitz <8703826+ethowitz@users.noreply.github.com> Date: Wed, 22 Jun 2022 16:23:48 -0400 Subject: [PATCH] feat: support multiple FxA JWKs to ease key rotation (#1339) --- docker-compose.e2e.mysql.yaml | 14 +++---- docker-compose.e2e.spanner.yaml | 14 +++---- syncstorage/src/settings.rs | 11 +++++ syncstorage/src/tokenserver/auth/oauth.rs | 49 +++++++++++++---------- syncstorage/src/tokenserver/settings.rs | 8 +++- 5 files changed, 59 insertions(+), 37 deletions(-) diff --git a/docker-compose.e2e.mysql.yaml b/docker-compose.e2e.mysql.yaml index 8ed81498bc..c1b9be51fa 100644 --- a/docker-compose.e2e.mysql.yaml +++ b/docker-compose.e2e.mysql.yaml @@ -34,13 +34,13 @@ services: SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: api-accounts.stage.mozaws.net SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET: secret0 SYNC_TOKENSERVER__RUN_MIGRATIONS: "true" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__KTY: "RSA" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__ALG: "RS256" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__KID: "20190730-15e473fd" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__FXA_CREATED_AT: "1564502400" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__USE: "sig" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__N: "15OpVGC7ws_SlU0gRbRh1Iwo8_gR8ElX2CDnbN5blKyXLg-ll0ogktoDXc-tDvTabRTxi7AXU0wWQ247odhHT47y5uz0GASYXdfPponynQ_xR9CpNn1eEL1gvDhQN9rfPIzfncl8FUi9V4WMd5f600QC81yDw9dX-Z8gdkru0aDaoEKF9-wU2TqrCNcQdiJCX9BISotjz_9cmGwKXFEekQNJWBeRQxH2bUmgwUK0HaqwW9WbYOs-zstNXXWFsgK9fbDQqQeGehXLZM4Cy5Mgl_iuSvnT3rLzPo2BmlxMLUvRqBx3_v8BTtwmNGA0v9O0FJS_mnDq0Iue0Dz8BssQCQ" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__E: "AQAB" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY: "RSA" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG: "RS256" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID: "20190730-15e473fd" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT: "1564502400" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE: "sig" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N: "15OpVGC7ws_SlU0gRbRh1Iwo8_gR8ElX2CDnbN5blKyXLg-ll0ogktoDXc-tDvTabRTxi7AXU0wWQ247odhHT47y5uz0GASYXdfPponynQ_xR9CpNn1eEL1gvDhQN9rfPIzfncl8FUi9V4WMd5f600QC81yDw9dX-Z8gdkru0aDaoEKF9-wU2TqrCNcQdiJCX9BISotjz_9cmGwKXFEekQNJWBeRQxH2bUmgwUK0HaqwW9WbYOs-zstNXXWFsgK9fbDQqQeGehXLZM4Cy5Mgl_iuSvnT3rLzPo2BmlxMLUvRqBx3_v8BTtwmNGA0v9O0FJS_mnDq0Iue0Dz8BssQCQ" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E: "AQAB" TOKENSERVER_HOST: http://localhost:8000 entrypoint: > /bin/sh -c " diff --git a/docker-compose.e2e.spanner.yaml b/docker-compose.e2e.spanner.yaml index 13fcdf407d..71b5d88a2b 100644 --- a/docker-compose.e2e.spanner.yaml +++ b/docker-compose.e2e.spanner.yaml @@ -35,13 +35,13 @@ services: SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: api-accounts.stage.mozaws.net SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET: secret0 SYNC_TOKENSERVER__RUN_MIGRATIONS: "true" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__KTY: "RSA" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__ALG: "RS256" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__KID: "20190730-15e473fd" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__FXA_CREATED_AT: "1564502400" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__USE: "sig" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__N: "15OpVGC7ws_SlU0gRbRh1Iwo8_gR8ElX2CDnbN5blKyXLg-ll0ogktoDXc-tDvTabRTxi7AXU0wWQ247odhHT47y5uz0GASYXdfPponynQ_xR9CpNn1eEL1gvDhQN9rfPIzfncl8FUi9V4WMd5f600QC81yDw9dX-Z8gdkru0aDaoEKF9-wU2TqrCNcQdiJCX9BISotjz_9cmGwKXFEekQNJWBeRQxH2bUmgwUK0HaqwW9WbYOs-zstNXXWFsgK9fbDQqQeGehXLZM4Cy5Mgl_iuSvnT3rLzPo2BmlxMLUvRqBx3_v8BTtwmNGA0v9O0FJS_mnDq0Iue0Dz8BssQCQ" - SYNC_TOKENSERVER__FXA_OAUTH_JWK__E: "AQAB" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY: "RSA" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG: "RS256" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID: "20190730-15e473fd" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT: "1564502400" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE: "sig" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N: "15OpVGC7ws_SlU0gRbRh1Iwo8_gR8ElX2CDnbN5blKyXLg-ll0ogktoDXc-tDvTabRTxi7AXU0wWQ247odhHT47y5uz0GASYXdfPponynQ_xR9CpNn1eEL1gvDhQN9rfPIzfncl8FUi9V4WMd5f600QC81yDw9dX-Z8gdkru0aDaoEKF9-wU2TqrCNcQdiJCX9BISotjz_9cmGwKXFEekQNJWBeRQxH2bUmgwUK0HaqwW9WbYOs-zstNXXWFsgK9fbDQqQeGehXLZM4Cy5Mgl_iuSvnT3rLzPo2BmlxMLUvRqBx3_v8BTtwmNGA0v9O0FJS_mnDq0Iue0Dz8BssQCQ" + SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E: "AQAB" TOKENSERVER_HOST: http://localhost:8000 entrypoint: > /bin/sh -c " diff --git a/syncstorage/src/settings.rs b/syncstorage/src/settings.rs index eda1bc86d6..b5f98b9e4d 100644 --- a/syncstorage/src/settings.rs +++ b/syncstorage/src/settings.rs @@ -232,6 +232,17 @@ impl Settings { "https://oauth.stage.mozaws.net", )?; s.set_default("tokenserver.fxa_oauth_request_timeout", 10)?; + + // The type parameter for None:: below would more appropriately be `Jwk`, but due + // to constraints imposed by version 0.11 of the config crate, it is not possible to + // implement `ValueKind: From`. The next best thing would be to use `ValueKind`, + // but `ValueKind` is private in this version of config. We use `bool` as a placeholder, + // since `ValueKind: From` is implemented, and None:: for all T is simply + // converted to ValueKind::Nil (see below link). + // https://github.com/mehcode/config-rs/blob/0.11.0/src/value.rs#L35 + s.set_default("tokenserver.fxa_oauth_primary_jwk", None::)?; + s.set_default("tokenserver.fxa_oauth_secondary_jwk", None::)?; + s.set_default("tokenserver.node_type", "spanner")?; s.set_default("tokenserver.statsd_label", "syncstorage.tokenserver")?; s.set_default("tokenserver.run_migrations", cfg!(test))?; diff --git a/syncstorage/src/tokenserver/auth/oauth.rs b/syncstorage/src/tokenserver/auth/oauth.rs index 6f7093279b..baf29be459 100644 --- a/syncstorage/src/tokenserver/auth/oauth.rs +++ b/syncstorage/src/tokenserver/auth/oauth.rs @@ -3,7 +3,6 @@ use futures::TryFutureExt; use pyo3::{ prelude::{Py, PyAny, PyErr, PyModule, Python}, types::{IntoPyDict, PyString}, - IntoPy, }; use serde::{Deserialize, Serialize}; use serde_json; @@ -11,7 +10,7 @@ use tokenserver_common::error::TokenserverError; use tokio::{task, time}; use super::VerifyToken; -use crate::tokenserver::settings::Settings; +use crate::tokenserver::settings::{Jwk, Settings}; use core::time::Duration; use std::convert::TryFrom; @@ -47,24 +46,31 @@ impl TryFrom<&Settings> for Verifier { let module = PyModule::from_code(py, code, Self::FILENAME, Self::FILENAME)?; let kwargs = { let dict = [("server_url", &settings.fxa_oauth_server_url)].into_py_dict(py); - let jwks = settings - .fxa_oauth_jwk - .as_ref() - .map(|jwk| { - let dict = [ - ("kty", &jwk.kty), - ("alg", &jwk.alg), - ("kid", &jwk.kid), - ("use", &jwk.use_of_key), - ("n", &jwk.n), - ("e", &jwk.e), - ] - .into_py_dict(py); - dict.set_item("fxa-createdAt", jwk.fxa_created_at).unwrap(); - - [dict] - }) - .into_py(py); + let parse_jwk = |jwk: &Jwk| { + let dict = [ + ("kty", &jwk.kty), + ("alg", &jwk.alg), + ("kid", &jwk.kid), + ("use", &jwk.use_of_key), + ("n", &jwk.n), + ("e", &jwk.e), + ] + .into_py_dict(py); + dict.set_item("fxa-createdAt", jwk.fxa_created_at).unwrap(); + + dict + }; + + let jwks = match ( + &settings.fxa_oauth_primary_jwk, + &settings.fxa_oauth_secondary_jwk, + ) { + (Some(primary_jwk), Some(secondary_jwk)) => { + Some(vec![parse_jwk(primary_jwk), parse_jwk(secondary_jwk)]) + } + (Some(jwk), None) | (None, Some(jwk)) => Some(vec![parse_jwk(jwk)]), + (None, None) => None, + }; dict.set_item("jwks", jwks).unwrap(); dict }; @@ -84,7 +90,8 @@ impl TryFrom<&Settings> for Verifier { Ok(Self { inner, timeout: settings.fxa_oauth_request_timeout, - jwk_is_cached: settings.fxa_oauth_jwk.is_some(), + jwk_is_cached: settings.fxa_oauth_primary_jwk.is_some() + || settings.fxa_oauth_secondary_jwk.is_some(), }) } } diff --git a/syncstorage/src/tokenserver/settings.rs b/syncstorage/src/tokenserver/settings.rs index 35057a2d0c..1b830be8f7 100644 --- a/syncstorage/src/tokenserver/settings.rs +++ b/syncstorage/src/tokenserver/settings.rs @@ -30,7 +30,10 @@ pub struct Settings { /// The JWK to be used to verify OAuth tokens. Passing a JWK to the PyFxA Python library /// prevents it from making an external API call to FxA to get the JWK, yielding substantial /// performance benefits. - pub fxa_oauth_jwk: Option, + pub fxa_oauth_primary_jwk: Option, + /// A secondary JWK to be used to verify OAuth tokens. This is intended to be used to enable + /// seamless key rotations on FxA. + pub fxa_oauth_secondary_jwk: Option, /// The issuer expected in the BrowserID verification response. pub fxa_browserid_issuer: String, /// The audience to be sent to the FxA BrowserID verification server. @@ -80,7 +83,8 @@ impl Default for Settings { fxa_metrics_hash_secret: "secret".to_owned(), fxa_oauth_server_url: "https://oauth.stage.mozaws.net".to_owned(), fxa_oauth_request_timeout: 10, - fxa_oauth_jwk: None, + fxa_oauth_primary_jwk: None, + fxa_oauth_secondary_jwk: None, fxa_browserid_audience: "https://token.stage.mozaws.net".to_owned(), fxa_browserid_issuer: "api-accounts.stage.mozaws.net".to_owned(), fxa_browserid_server_url: "https://verifier.stage.mozaws.net/v2".to_owned(),