Skip to content

Commit

Permalink
Make JWT key optional by generating it on the fly if unspecified (#820)
Browse files Browse the repository at this point in the history
See commit messages.

Fixes #634
  • Loading branch information
owi92 committed May 12, 2023
2 parents 265bbae + 4d74535 commit 7ce53a8
Show file tree
Hide file tree
Showing 10 changed files with 56 additions and 59 deletions.
51 changes: 27 additions & 24 deletions backend/src/auth/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,16 @@ use super::User;

#[derive(Debug, Clone, confique::Config)]
pub(crate) struct JwtConfig {
/// Signing algorithm for JWTs. Prefer `ES` style algorithms over others.
/// The algorithm choice has to be configured in Opencast as well.
/// Signing algorithm for JWTs.
///
/// Valid values: "ES256", "ES384"
#[config(default = "ES384")]
signing_algorithm: Algorithm,

/// Path to the secret signing key. The key has to be PEM encoded.
///
/// # For `ES*` algorithms
///
/// Has to be an EC key encoded as PKCS#8. To generate such a key, you can
/// run the following commands. You can replace `secp256r1` with other
/// supported values like `secp384r1`.
///
/// openssl ecparam -name secp256r1 -genkey -noout -out sec1.pem
/// openssl pkcs8 -topk8 -nocrypt -in sec1.pem -out private-key.pem
///
/// Here, the `sec1.pem` is encoded as SEC1 instead of PKCS#8. The second
/// command converts the key.
pub(crate) secret_key: PathBuf,

/// Path to the secret signing key. The key has to be PEM encoded. If not
/// specified, a key is generated everytime Tobira is started. The randomly
/// generated key is fine for most use cases.
pub(crate) secret_key: Option<PathBuf>,

/// The duration for which a JWT is valid. JWTs are just used as temporary
/// ways to authenticate against Opencast, so they just have to be valid
Expand Down Expand Up @@ -127,13 +116,27 @@ impl JwtContext {

impl JwtConfig {
fn load_auth(&self) -> Result<JwtAuth> {
let pem_encoded = std::fs::read(&self.secret_key)
.context("could not load secret key file")?;
let (_label, bytes) = pem_rfc7468::decode_vec(&pem_encoded)
.context("secret key file is not a valid PEM encoded key")?;

match self.signing_algorithm {
algo @ (Algorithm::ES256 | Algorithm::ES384) => JwtAuth::load_es(algo, &bytes),
if let Some(secret_key_path) = &self.secret_key {
let pem_encoded = std::fs::read(secret_key_path)
.context("could not load secret key file")?;
let (_label, pkcs8_bytes) = pem_rfc7468::decode_vec(&pem_encoded)
.context("secret key file is not a valid PEM encoded key")?;
JwtAuth::load_es(self.signing_algorithm, &pkcs8_bytes)
} else {
let ring_algo = match self.signing_algorithm {
Algorithm::ES256 => &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
Algorithm::ES384 => &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING,
};

info!(
"No JWT key specified, generating key for algorithm {}",
self.signing_algorithm.to_str(),
);
let rng = ring::rand::SystemRandom::new();
let pkcs8_bytes = ring::signature::EcdsaKeyPair::generate_pkcs8(ring_algo, &rng)
.context("failed to generate JWT ECDSA key")?;

JwtAuth::load_es(self.signing_algorithm, pkcs8_bytes.as_ref())
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions backend/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,7 @@ pub(crate) struct AuthConfig {
#[config(nested)]
pub(crate) login_page: LoginPageConfig,

/// JWT configuration. JWTs are only used to automatically authenticate
/// users against Opencast with short-lived tokens. They are not used for
/// user sessions.
/// JWT configuration. See documentation for more information.
#[config(nested)]
pub(crate) jwt: JwtConfig,

Expand Down
2 changes: 1 addition & 1 deletion backend/src/cmd/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ async fn check_referenced_files(config: &Config) -> Result<()> {

let mut files = vec![
&config.theme.favicon,
&config.auth.jwt.secret_key,
&config.theme.logo.large.path,
];
files.extend(config.theme.logo.small.as_ref().map(|l| &l.path));
files.extend(config.auth.jwt.secret_key.as_ref());

for path in files {
debug!("Trying to open '{}' for reading...", path.display());
Expand Down
4 changes: 3 additions & 1 deletion backend/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ impl Config {
fix_path(&base, &mut logo.path);
}
fix_path(&base, &mut self.theme.favicon);
fix_path(&base, &mut self.auth.jwt.secret_key);
if let Some(jwt_key) = &mut self.auth.jwt.secret_key {
fix_path(&base, jwt_key);
}

Ok(())
}
Expand Down
2 changes: 0 additions & 2 deletions docs/docs/setup/ansible-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ Also not shown here: **setup regular database backups!**
- logo-large.svg
- logo-small.svg
- favicon.svg
- jwt-key.pem
notify: restart tobira

- name: Deploy configuration
Expand Down Expand Up @@ -220,7 +219,6 @@ The scripts assume the following files to exist:
- `logo-large.svg`
- `logo-small.svg`
- `favicon.svg`
- `jwt-key.pem`
- `templates/`
- `config.toml` (see [Configuration docs](./config))

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/setup/auth/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sidebar_position: 8
The topic of authentication and authorization comes up in the context of Tobira in several situations, each described in its own section in this document:

- [Users authenticating themselves against Tobira](#user-login) (i.e. logging into Tobira)
- [Tobira authenticating against Opencast](#tobira-against-opencast) (used for for syncing)
- [Tobira authenticating against Opencast](#tobira-against-opencast) (used for syncing)
- [Opencast authenticating against Tobira](#opencast-against-tobira) (used to change pages from within the Opencast admin UI)
- [Tobira cross-authenticating its users against Opencast](#cross-auth-users-against-opencast) (used for the uploader, Studio and the editor)

Expand Down
18 changes: 15 additions & 3 deletions docs/docs/setup/auth/jwt.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ The "trust" in this solution comes from you telling Opencast to trust a specific

## Setup Tobira

For Tobira, you only need to configure some values in `auth.jwt`, for example:
For Tobira, you don't necessarily need to configure anything.
By default, a suitable signing algorithm is selected and a secret key is generated whenever Tobira is started.
That's sufficient for most use cases.
If you run multiple Tobira instances (for redundency), you need to specify a key manually as otherwise, each instance has a different key.

The algorithm and private key can be specified in `[auth.jwt]`.

<details>
<summary>Specifying key manually</summary>

```toml
[auth.jwt]
Expand All @@ -21,7 +29,7 @@ secret_key = "jwt-key.pem"
```

The secret key has to be a key matching the algorithm.
For `ES256`, that's an EC key encoded as PKCS#8.
For `ES*`, that's an EC key encoded as PKCS#8.
To generate such a key, you can run these commands (you can replace `secp256r1` with other supported values like `secp384r1`):

```
Expand All @@ -31,13 +39,17 @@ openssl pkcs8 -topk8 -nocrypt -in sec1.pem -out private-key.pem

Here, the `sec1.pem` is encoded as SEC1 instead of PKCS#8. The second command converts the key.

**Important**: the expiration time for the JWT should be chosen to be fairly short to reduce the security risk posed by a stolen JWT.
</details>

:::caution
The expiration time for the JWT should be chosen to be fairly short to reduce the security risk posed by a stolen JWT.
Tobira generates a new JWT right before every request it sends to Opencast.
So you should only need to account for network delay and clock skew.
The default of 30 seconds should be fine for most installations,
but you can try to be more conservative if you want.
We strongly recommend against going higher, though. If you need that for some reason,
you should probably rather try to mitigate the underlying problem.
:::


## Setup Opencast
Expand Down
29 changes: 7 additions & 22 deletions docs/docs/setup/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -243,33 +243,18 @@
#note =


# JWT configuration. JWTs are only used to automatically authenticate
# users against Opencast with short-lived tokens. They are not used for
# user sessions.
# JWT configuration. See documentation for more information.
[auth.jwt]
# Signing algorithm for JWTs. Prefer `ES` style algorithms over others.
# The algorithm choice has to be configured in Opencast as well.
# Signing algorithm for JWTs.
#
# Valid values: "ES256", "ES384"
#
# Required! This value must be specified.
#signing_algorithm =
# Default value: "ES384"
#signing_algorithm = "ES384"

# Path to the secret signing key. The key has to be PEM encoded.
#
# # For `ES*` algorithms
#
# Has to be an EC key encoded as PKCS#8. To generate such a key, you can
# run the following commands. You can replace `secp256r1` with other
# supported values like `secp384r1`.
#
# openssl ecparam -name secp256r1 -genkey -noout -out sec1.pem
# openssl pkcs8 -topk8 -nocrypt -in sec1.pem -out private-key.pem
#
# Here, the `sec1.pem` is encoded as SEC1 instead of PKCS#8. The second
# command converts the key.
#
# Required! This value must be specified.
# Path to the secret signing key. The key has to be PEM encoded. If not
# specified, a key is generated everytime Tobira is started. The randomly
# generated key is fine for most use cases.
#secret_key =

# The duration for which a JWT is valid. JWTs are just used as temporary
Expand Down
1 change: 0 additions & 1 deletion docs/docs/setup/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ A few additional notes and tips about that:

- Most values are optional and don't need to be adjusted for most use cases.
- Don't touch any `auth.*` values for now. Authentication is handled in the next step.
- Except for `auth.jwt.*`: See [Setup JWT auth](./auth/jwt) for that.
- You can check the configuration file and all connections by running `tobira check`.


Expand Down
2 changes: 1 addition & 1 deletion docs/docs/setup/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sidebar_position: 1
To run, Tobira requires:

- A Unix system.
- A **PostgreSQL** (≥10) database (see below for further requirements).
- A **PostgreSQL** (≥11) database (see below for further requirements).
- [**Meilisearch**](https://www.meilisearch.com/) (≥ v1.1). For installation, see [Meili's docs](https://docs.meilisearch.com/learn/getting_started/quick_start.html#step-1-setup-and-installation).
- An **Opencast** that satisfies certain condition. See below.

Expand Down

0 comments on commit 7ce53a8

Please sign in to comment.