Skip to content

Commit

Permalink
feat(rest): authenticate clients
Browse files Browse the repository at this point in the history
Provide the ability to authenticate client requests through the use of a
JSON Web Token (JWT). This requires a JSON Web Key (JWK) to be available
for use by the REST service so that it can validate the JWT.

The REST service can be started with the '--no-auth' argument to disable
authentication. This is useful for test cases where authentication is
unnecessary.

The REST service can be started with the '--jwk' argument to enable
authentication. The path to the relevant JWK file must also be provided.

Example authentication files are provided in
../Mayastor/control-plane/rest/authentication for test purposes and
should not be used in production.
  • Loading branch information
Paul Yoong committed Mar 4, 2021
1 parent 2644d1a commit 30c792d
Show file tree
Hide file tree
Showing 20 changed files with 449 additions and 36 deletions.
52 changes: 50 additions & 2 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions control-plane/deployer/src/infra/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ impl ComponentAction for Rest {
Binary::from_dbg("rest")
.with_nats("-n")
.with_arg("--dummy-certificates")
.with_arg("--no-auth")
.with_args(vec!["--https", "rest:8080"])
.with_args(vec!["--http", "rest:8081"]),
)
Expand All @@ -36,6 +37,7 @@ impl ComponentAction for Rest {
Binary::from_dbg("rest")
.with_nats("-n")
.with_arg("--dummy-certificates")
.with_arg("--no-auth")
.with_args(vec!["-j", &jaeger_config])
.with_args(vec!["--https", "rest:8080"])
.with_args(vec!["--http", "rest:8081"]),
Expand Down
13 changes: 12 additions & 1 deletion control-plane/macros/actix/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ impl Method {
let handler: ItemFn = syn::parse(item)?;
Ok(handler.sig.ident)
}
/// Add authorisation to handler functions by adding a BearerToken as an
/// additional function argument.
/// The BearerToken is defined in
/// ../Mayastor/control-plane/rest/service/src/v0/mod.rs
fn handler_fn_with_auth(item: TokenStream) -> syn::Result<syn::ItemFn> {
let mut func: ItemFn = syn::parse(item)?;
let new_input = syn::parse_str("_token: BearerToken")?;
func.sig.inputs.push(new_input);
Ok(func)
}
fn generate(
&self,
attr: TokenStream,
Expand All @@ -65,7 +75,8 @@ impl Method {
let full_uri: TokenStream2 = Self::handler_uri(attr.clone()).into();
let relative_uri: TokenStream2 = Self::openapi_uri(attr.clone()).into();
let handler_name = Self::handler_name(item.clone())?;
let handler_fn: TokenStream2 = item.into();
let handler_fn: TokenStream2 =
Self::handler_fn_with_auth(item)?.to_token_stream();
let method: TokenStream2 = self.method().parse()?;
let variant: TokenStream2 = self.variant().parse()?;
let handler_name_str = handler_name.to_string();
Expand Down
1 change: 1 addition & 0 deletions control-plane/rest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ paperclip = { version = "0.5.0", default-features = false, optional = true }
macros = { path = "../macros" }
http = "0.2.3"
tinytemplate = { version = "1.2" }
jsonwebtoken = "7.2.0"

[dev-dependencies]
composer = { path = "../../composer" }
Expand Down
23 changes: 23 additions & 0 deletions control-plane/rest/authentication/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**WARNING**: These are dummy example RSA keys and should not be used in production.

There are various websites (such as https://russelldavies.github.io/jwk-creator/) which provide the capability of generating the JSON Web Key from the public RSA key.
For convenience the 'jwk' file has already been generated from the provided public key.

# Usage
To try out the dummy JSON Web Key (JWK), execute the following steps from within the nix-shell:
1. Run the deployer without launching the rest service
```bash
./target/debug/deployer start -a "Node, Pool, Volume" --no-rest
```
2. Start the REST service within the nix-shell
```bash
./target/debug/rest --dummy-certificates --jwk "../Mayastor/control-plane/rest/authentication/jwk"
```
2. Set the token value (located in ../Mayastor/control-plane/rest/authentication/token)
```bash
export TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJyYW5kb20gc3ViamVjdCIsImNvbXBhbnkiOiJteSBjb21wYW55IiwiZXhwIjoxMDAwMDAwMDAwMH0.GkcWHAJ4-qXihaR2j8ZvJgFB1OPpo9P5PkauTmb4PHvlDTYpDQy_nfTHmZCKHS1WEBtsH-HOXApKf32oJEU0K_2SAO76PVZrqvfMewccny-aB9gyu6WMlgSWK8wvGq4h_t_Ma4KIBlPv5PCQO1fyv9bWM3Y3Lu2rPxvNg0O_V_mfnq_Ynwcy4qhnZmse8pZ9zJJaM5OPv2ucWRPKWNzSX8OOz11MGBcdV5QBM-eBpjeSvejEwQ1xOxfiwZwZosFKjPnwMWn8dirMhMNqyRwWgjmOFU2hpc13Ik2VcSWEKTF4ndoUmMLXmCmQ2pSrn9MihEfkpO_VHx_sRVtmYVe2R4iy7ocul3eG7ZAvRq-_GIqBpwbcdUPANIyEFWUWgiPB5_kFvf4-iIBip7NhZ0_4DVoqukYBM2XodejXY863p2frglljt23EimNoKlrtqyxw1wXcbsYtiqCsd3cFTMUkrVesu9xNQPfpM8so37SmTsrC1nOssGEiADAGowqu5SsS
```
3. Use curl to make a REST request using the above token
```bash
curl -X GET "https://localhost:8080/v0/nodes" -H "accept: application/json" -H "Authorization: Bearer ${TOKEN}" -k
```
39 changes: 39 additions & 0 deletions control-plane/rest/authentication/id_rsa
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
11 changes: 11 additions & 0 deletions control-plane/rest/authentication/id_rsa.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN PUBLIC KEY-----
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAtTtUE2YgN2te7Hd29BZx
eGjmagg0Ch9zvDIlHRjl7Y6Y9Gankign24dOXFC0t/3XzylySG0w56YkAgZPbu+7
NRUbjE8ev5gFEBVfHgXmPvFKwPSkCtZG94Kx+lK/BZ4oOieLSoqSSsCdm6Mr5q57
odkWghnXXohmRgKVgrg2OS1fUcw5l2AYljierf2vsFDGU6DU1PqeKiDrflsu8CFx
DBAkVdUJCZH5BJcUMhjK41FCyYImtEb13eXRIr46rwxOGjwj6Szthd+sZIDDP/VV
BJ3bGNk80buaWYQnojtllseNBg9pGCTBtYHB+kd+NNm2rwPWQLjmcY1ym9LtJmrQ
CXvA4EUgsG7qBNj1dl2NHcG03eEoJBejQ5xwTNgQZ6311lXuKByP5gkiLctCtwn1
wGTJpjbLKo8xReNdKgFqrIOT1mC76oZpT3AsWlVH60H4aVTthuYEBCJgBQh5Bh6y
44ANGcybj+q7sOOtuWi96sXNOCLczEbqKYpeuckYp1LPAgMBAAE=
-----END PUBLIC KEY-----
7 changes: 7 additions & 0 deletions control-plane/rest/authentication/jwk
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kty": "RSA",
"n": "tTtUE2YgN2te7Hd29BZxeGjmagg0Ch9zvDIlHRjl7Y6Y9Gankign24dOXFC0t_3XzylySG0w56YkAgZPbu-7NRUbjE8ev5gFEBVfHgXmPvFKwPSkCtZG94Kx-lK_BZ4oOieLSoqSSsCdm6Mr5q57odkWghnXXohmRgKVgrg2OS1fUcw5l2AYljierf2vsFDGU6DU1PqeKiDrflsu8CFxDBAkVdUJCZH5BJcUMhjK41FCyYImtEb13eXRIr46rwxOGjwj6Szthd-sZIDDP_VVBJ3bGNk80buaWYQnojtllseNBg9pGCTBtYHB-kd-NNm2rwPWQLjmcY1ym9LtJmrQCXvA4EUgsG7qBNj1dl2NHcG03eEoJBejQ5xwTNgQZ6311lXuKByP5gkiLctCtwn1wGTJpjbLKo8xReNdKgFqrIOT1mC76oZpT3AsWlVH60H4aVTthuYEBCJgBQh5Bh6y44ANGcybj-q7sOOtuWi96sXNOCLczEbqKYpeuckYp1LP",
"e": "AQAB",
"alg": "RS256",
"use": "sig"
}
1 change: 1 addition & 0 deletions control-plane/rest/authentication/token
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJyYW5kb20gc3ViamVjdCIsImNvbXBhbnkiOiJteSBjb21wYW55IiwiZXhwIjoxMDAwMDAwMDAwMH0.GkcWHAJ4-qXihaR2j8ZvJgFB1OPpo9P5PkauTmb4PHvlDTYpDQy_nfTHmZCKHS1WEBtsH-HOXApKf32oJEU0K_2SAO76PVZrqvfMewccny-aB9gyu6WMlgSWK8wvGq4h_t_Ma4KIBlPv5PCQO1fyv9bWM3Y3Lu2rPxvNg0O_V_mfnq_Ynwcy4qhnZmse8pZ9zJJaM5OPv2ucWRPKWNzSX8OOz11MGBcdV5QBM-eBpjeSvejEwQ1xOxfiwZwZosFKjPnwMWn8dirMhMNqyRwWgjmOFU2hpc13Ik2VcSWEKTF4ndoUmMLXmCmQ2pSrn9MihEfkpO_VHx_sRVtmYVe2R4iy7ocul3eG7ZAvRq-_GIqBpwbcdUPANIyEFWUWgiPB5_kFvf4-iIBip7NhZ0_4DVoqukYBM2XodejXY863p2frglljt23EimNoKlrtqyxw1wXcbsYtiqCsd3cFTMUkrVesu9xNQPfpM8so37SmTsrC1nOssGEiADAGowqu5SsS
2 changes: 1 addition & 1 deletion control-plane/rest/openapi-specs/v0_api_spec.json

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions control-plane/rest/service/src/authentication.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use actix_web::{Error, HttpRequest};
use jsonwebtoken::{crypto, Algorithm, DecodingKey};
use std::str::FromStr;

use http::HeaderValue;
use std::fs::File;

/// Initialise JWK with the contents of the file at 'jwk_path'.
/// If jwk_path is 'None', authentication is disabled.
pub fn init(jwk_path: Option<String>) -> JsonWebKey {
match jwk_path {
Some(path) => {
let jwk_file = File::open(path).expect("Failed to open JWK file");
let jwk = serde_json::from_reader(jwk_file)
.expect("Failed to deserialise JWK");
JsonWebKey {
jwk,
}
}
None => JsonWebKey {
..Default::default()
},
}
}

#[derive(Default, Debug)]
pub struct JsonWebKey {
jwk: serde_json::Value,
}

impl JsonWebKey {
// Returns true if REST calls should be authenticated.
fn auth_enabled(&self) -> bool {
!self.jwk.is_null()
}

// Return the algorithm.
fn algorithm(&self) -> Algorithm {
Algorithm::from_str(self.jwk["alg"].as_str().unwrap()).unwrap()
}

// Return the modulus.
fn modulus(&self) -> &str {
self.jwk["n"].as_str().unwrap()
}

// Return the exponent.
fn exponent(&self) -> &str {
self.jwk["e"].as_str().unwrap()
}

// Return the decoding key
fn decoding_key(&self) -> DecodingKey {
DecodingKey::from_rsa_components(self.modulus(), self.exponent())
}
}

/// Authenticate the HTTP request by checking the authorisation token to ensure
/// the sender is who they claim to be.
pub fn authenticate(req: &HttpRequest) -> Result<(), Error> {
let jwk: &JsonWebKey = req.app_data().unwrap();

// If authentication is disabled there is nothing to do.
if !jwk.auth_enabled() {
return Ok(());
}

match req.headers().get(http::header::AUTHORIZATION) {
Some(token) => validate(&format_token(token), jwk),
None => {
tracing::error!("Missing bearer token in HTTP request.");
Err(Error::from(actix_web::HttpResponse::Unauthorized()))
}
}
}

// Ensure the token is formatted correctly by removing the "Bearer" prefix if
// present.
fn format_token(token: &HeaderValue) -> String {
let token = token
.to_str()
.expect("Failed to convert token to string")
.replace("Bearer", "");
token.trim().into()
}

/// Validate a bearer token.
pub fn validate(token: &str, jwk: &JsonWebKey) -> Result<(), Error> {
let (message, signature) = split_token(&token);
return match crypto::verify(
&signature,
&message,
&jwk.decoding_key(),
jwk.algorithm(),
) {
Ok(true) => Ok(()),
Ok(false) => {
tracing::error!("Signature verification failed.");
Err(Error::from(actix_web::HttpResponse::Unauthorized()))
}
Err(e) => {
tracing::error!(
"Failed to complete signature verification with error {}",
e
);
Err(Error::from(actix_web::HttpResponse::Unauthorized()))
}
};
}

// Split the JSON Web Token (JWT) into 2 parts, message and signature.
// The message comprises the header and payload.
//
// JWT format:
// <header>.<payload>.<signature>
// \______ ________/
// \/
// message
fn split_token(token: &str) -> (String, String) {
let elems = token.split('.').collect::<Vec<&str>>();
let message = format!("{}.{}", elems[0], elems[1]);
let signature = elems[2];
(message, signature.into())
}

#[test]
fn validate_test() {
let token_file = std::env::current_dir()
.expect("Failed to get current directory")
.join("authentication")
.join("token");
let mut token = std::fs::read_to_string(token_file)
.expect("Failed to get bearer token");
let jwk_file = std::env::current_dir()
.expect("Failed to get current directory")
.join("authentication")
.join("jwk");
let jwk = init(Some(jwk_file.to_str().unwrap().into()));

validate(&token, &jwk).expect("Validation should pass");
// create invalid token
token.push_str("invalid");
validate(&token, &jwk)
.expect_err("Validation should fail with an invalid token");
}
Loading

0 comments on commit 30c792d

Please sign in to comment.