Skip to content

Commit

Permalink
Update auth server template with API version (#62)
Browse files Browse the repository at this point in the history
## Purpose
- Updates the custom auth server template with versioning support by
adding `api-version` as a query parameter
- Updates to hyper 1.2 and clap 4, with required refactor
- Updates and removes outdated documentation

## Does this introduce a breaking change?
<!-- Mark one with an "x". -->
```
[x] Yes
[ ] No
```
The new query parameter `api-version` is required for custom
authentication requests.

## Pull Request Type
What kind of change does this Pull Request introduce?

<!-- Please check the one that applies to this PR using "x". -->
```
[ ] Bugfix
[x] Feature
[ ] Code style update (formatting, local variables)
[x] Refactoring (no functional changes, no api changes)
[x] Documentation content changes
[ ] Other... Please describe:
```
  • Loading branch information
gordonwang0 authored Mar 11, 2024
1 parent 4344e61 commit 6b8668e
Show file tree
Hide file tree
Showing 10 changed files with 615 additions and 718 deletions.
818 changes: 342 additions & 476 deletions samples/auth-server-template/Cargo.lock

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions samples/auth-server-template/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ edition = "2021"

[dependencies]
chrono = "0.4"
clap = { version = "4", features = ["derive"] }
futures = "0.3"
futures-core = "0.3"
futures-util = "0.3"
hyper = { version = "0.14", features = ["http1", "server", "tcp"] }
http-body-util = "0.1"
hyper = { version = "1.2", features = ["http1", "server"] }
hyper-util = { version = "0.1", features = ["tokio"] }
openssl = "0.10"
structopt = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] }
tokio-openssl = "0.6"
2 changes: 1 addition & 1 deletion samples/auth-server-template/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM alpine:3.17
FROM alpine:3.19

RUN apk upgrade --no-cache && \
apk add --no-cache libgcc
Expand Down
30 changes: 19 additions & 11 deletions samples/auth-server-template/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# auth-server-template

This is a sample server for Azure IoT MQ broker custom authentication.
This is a sample server for Azure IoT MQ broker custom authentication.

The custom authentication feature of the IoT MQ MQTT brokers provides an extension point that allows customers to plug in an authentication server to authenticate clients that connect to the MQTT broker. The server here is intended as a sample for how such a custom authentication can be implemented.
The custom authentication feature of the IoT MQ broker provides an extension point that allows customers to plug in an authentication server to authenticate clients that connect to the MQTT broker. The server here is intended as a sample for how such a custom authentication can be implemented.

## Deploying the template

A private preview image of the template is available with the tag `e4kpreview.azurecr.io/auth-server-template:latest`. You only need to deploy the template yourself once you make modifications.
A private preview image of the template is available with the tag `ghcr.io/azure-samples/explore-iot-operations/auth-server-template:0.5.0`. You only need to deploy the template yourself once you make modifications.

The template server needs to be built and deployed as a Kubernetes service. You will need to provide the repository for hosting its container image.

Expand Down Expand Up @@ -36,19 +36,27 @@ kubectl apply -f ./deploy/auth-server-template.yaml

## Using the template

Add the custom authentication method to your IoT MQ BrokerAuthentication resource. An example of the fields to add is below:
Add an MQ BrokerAuthentication with custom authentication. An example BrokerAuthentication is below:

```yaml
authenticationMethods:
- custom:;
apiVersion: mq.iotoperations.azure.com/v1beta1
kind: BrokerAuthentication
metadata:
name: custom-authn
spec:
listenerRef:
- listener
authenticationMethods:
- method: custom
# Required, must match the pod name in auth-server-template.yaml
endpoint: https://auth-server-template
# Required, as this template uses a self-signed CA.
ca_cert: custom-auth-ca
# Optional; this example uses the client cert generated by make_credentials.sh
auth:
x509:
secretName: custom-auth-client-cert
type: x509
source: k8s
secret: custom-auth-client-cert
# Required, as this template uses a self-signed CA.
ca_cert: custom-auth-ca
```
The auth server template will approve authentication requests from all clients, except clients providing usernames that start with `deny`.
The auth server template will approve authentication requests from all clients, except clients providing usernames that start with `deny`. It will set a credential expiration of 10 seconds for any clients providing usernames that start with `expire`.
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
openapi: 3.0.3
info:
title: IoT MQ Custom Authentication API
title: Azure IoT Operations MQ Custom Authentication API
description: |-
This is the HTTP API used for custom authentication. Custom authentication allows you to extend client authentication beyond the provided authentication methods. With custom authentication, the IoT MQ broker will forward the credentials of connecting clients to an external custom authentication server. The custom authentication server will decide whether to accept clients and determine each client's authorization attributes.
The IoT MQ broker will make requests to the custom authentication server with the formats specified below. Likewise, the custom authentication server must respond with the specified response format.
This API is currently in preview, and may change before GA.
version: preview
version: 0.5.0
servers:
- url: https://custom-auth-endpoint/
description: User-specified endpoint
Expand All @@ -23,6 +21,14 @@ paths:
operationId: authClient
description: |-
This request from the IoT MQ broker to the custom authentication server forwards the connecting client's credentials to the custom authentication server. The custom authentication server must then use the provided information to accept or reject the client.
parameters:
- in: query
name: api-version
description: API version of custom authentication API
schema:
type: string
example: "0.5.0"
required: true
requestBody:
description: |-
Request from the IoT MQ broker to the custom authentication server. The request body contains the information from either an MQTT CONNECT or AUTH packet.
Expand Down Expand Up @@ -73,6 +79,12 @@ paths:
text/plain:
schema:
$ref: '#/components/schemas/ErrorResponse'
422:
description: Unprocessable content in request. Returned when the custom authentication API version in the request is unsupported by the server. The response body will contain the server's supported API versions.
content:
application/json:
schema:
$ref: '#/components/schemas/UnsupportedVersionResponse'
components:
schemas:
AuthRequest:
Expand Down Expand Up @@ -143,3 +155,15 @@ components:
type: string
description: Error message for the IoT MQ broker to log
example: error message
UnsupportedVersionResponse:
type: object
description: Returned when a request's api-version is unsupported
properties:
supportedVersions:
type: array
items:
type: string
description: Supported custom authentication API versions. The MQTT broker should automatically retry with one of these versions.
example: ["0.5.0"]
required:
- supportedVersions
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ spec:
containers:
- name: auth-server-template
# Change this if you are using your own container registry.
image: e4kpreview.azurecr.io/auth-server-template:latest
image: ghcr.io/azure-samples/explore-iot-operations/auth-server-template:0.5.0
imagePullPolicy: Always
ports:
- name: https
Expand Down
65 changes: 50 additions & 15 deletions samples/auth-server-template/src/authenticate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,64 @@

use std::collections::BTreeMap;

use hyper::{header, Method, StatusCode};
use openssl::x509::X509;

use crate::http::{ParsedRequest, Response};

/// Returned when the client requests an invalid API version. Contains a list of
/// supported API versions.
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct SupportedApiVersions {
/// List of supported API versions.
supported_versions: Vec<String>,
}

impl Default for SupportedApiVersions {
fn default() -> Self {
SupportedApiVersions {
supported_versions: vec!["0.5.0".to_string()],
}
}
}

/// Authenticate the connecting MQTT client.
pub(crate) async fn authenticate(req: ParsedRequest) -> Response {
// Check that the request follows the authentication spec.
if req.method != hyper::Method::POST {
if req.method != Method::POST {
return Response::method_not_allowed(&req.method);
}

let body = if let Some(body) = req.body {
body
} else {
if let Some(content_type) = req.headers.get(header::CONTENT_TYPE.as_str()) {
if content_type.to_lowercase() != "application/json" {
return Response::bad_request(format!("invalid content-type: {content_type}"));
}
}

let Some(body) = req.body else {
return Response::bad_request("missing body");
};

if req.uri != "/" {
return Response::not_found(format!("{} not found", req.uri));
if req.path != "/" {
return Response::not_found(format!("{} not found", req.path));
}

if let Some(api_version) = req.query.get("api-version") {
// Currently, the custom auth API supports only version 0.5.0.
if api_version != "0.5.0" {
return Response::json(
StatusCode::UNPROCESSABLE_ENTITY,
SupportedApiVersions::default(),
);
}
} else {
return Response::bad_request("missing api-version");
}

let body: ClientAuthRequest = match serde_json::from_str(&body) {
Ok(body) => body,
Err(err) => return Response::bad_request(format!("invalid client request body: {}", err)),
Err(err) => return Response::bad_request(format!("invalid client request body: {err}")),
};

Response::from(auth_client(body).await)
Expand Down Expand Up @@ -96,19 +130,22 @@ struct AuthPassResponse {
impl From<ClientAuthResponse> for Response {
fn from(response: ClientAuthResponse) -> Response {
match response {
ClientAuthResponse::Allow(response) => Response::json(hyper::StatusCode::OK, response),
ClientAuthResponse::Allow(response) => Response::json(StatusCode::OK, response),

ClientAuthResponse::Deny { reason } => {
let body = serde_json::json!({
"reason": reason,
});

Response::json(hyper::StatusCode::FORBIDDEN, body)
Response::json(StatusCode::FORBIDDEN, body)
}
}
}
}

// This implementation is a placeholder. The actual implementation may need to be async, so allow unused async
// in the signature.
#[allow(clippy::unused_async)]
async fn auth_client(body: ClientAuthRequest) -> ClientAuthResponse {
match body {
ClientAuthRequest::Connect {
Expand All @@ -118,15 +155,12 @@ async fn auth_client(body: ClientAuthRequest) -> ClientAuthResponse {
} => {
// TODO: Authenticate the client with provided credentials. For now, this template just logs the
// credentials. Note the password is base64-encoded.
println!(
"Got MQTT CONNECT; username: {:?}, password: {:?}",
username, password
);
println!("Got MQTT CONNECT; username: {username:?}, password: {password:?}");

// TODO: Authenticate the client with provided certs. For now, this template just logs the certs.
if let Some(certs) = certs {
println!("Got certs:");
println!("{:#?}", certs);
println!("{certs:#?}");
}

// TODO: Get attributes associated with the presented certificate. For now, this template
Expand All @@ -139,7 +173,8 @@ async fn auth_client(body: ClientAuthRequest) -> ClientAuthResponse {
// expiry and allows clients to remain connected indefinitely.
let example_expiry = username.as_ref().and_then(|username| {
if username.starts_with("expire") {
let example_expiry = chrono::Utc::now() + chrono::Duration::seconds(10);
let example_expiry = chrono::Utc::now()
+ chrono::TimeDelta::try_seconds(10).expect("invalid hardcoded time value");

Some(example_expiry.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
} else {
Expand Down
Loading

0 comments on commit 6b8668e

Please sign in to comment.