Skip to content
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

Update doc to include IS-10 #364

Merged
merged 23 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions Development/nmos-cpp-node/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,11 @@ int main(int argc, char* argv[])
}
#endif

// only implement communication with Authorization server if IS-10/BCP-003-02 is required
// cf. preprocessor conditions in nmos::make_node_api, nmos::make_connection_api, nmos::make_events_api, nmos::make_channelmapping_api, make_events_ws_validate_handler
// only configure communication with Authorization server if IS-10/BCP-003-02 is required
// Note:
// the validate_authorization callback must be set up before executing the make_node_server where make_node_api, make_connection_api, make_events_api, and make_channelmapping_api are set up
// the ws_validate_authorization callback must be set up before executing the make_node_server where make_events_ws_validate_handler is set up
// the get_authorization_bearer_token callback must be set up before executing the make_node_server where make_http_client_config is set up
nmos::experimental::authorization_state authorization_state;
if (nmos::experimental::fields::server_authorization(node_model.settings))
{
Expand Down Expand Up @@ -153,7 +156,7 @@ int main(int argc, char* argv[])
}
#endif

// only implement communication with Authorization server if IS-10/BCP-003-02 is required
// only configure communication with Authorization server if IS-10/BCP-003-02 is required
if (nmos::experimental::fields::client_authorization(node_model.settings))
{
std::map<nmos::host_port, web::http::experimental::listener::api_router> api_routers;
Expand Down Expand Up @@ -214,7 +217,7 @@ int main(int argc, char* argv[])
}
}

// only implement communication with Authorization server if IS-10/BCP-003-02 is required
// only configure communication with Authorization server if IS-10/BCP-003-02 is required
if (nmos::experimental::fields::client_authorization(node_model.settings) || nmos::experimental::fields::server_authorization(node_model.settings))
{
// IS-10 client registration, fetch access token, and fetch authorization server token public key
Expand Down
8 changes: 5 additions & 3 deletions Development/nmos-cpp-registry/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,10 @@ int main(int argc, char* argv[])
}
#endif

// only implement communication with Authorization server if IS-10/BCP-003-02 is required
// cf. preprocessor conditions in nmos::make_registration_api, nmos::make_query_api, nmos::make_query_ws_validate_handler, nmos::make_events_ws_validate_handler
// only configure communication with Authorization server if IS-10/BCP-003-02 is required
// Note:
// the validate_authorization callback must be set up before executing the make_node_server where make_node_api, make_connection_api, make_events_api, and make_channelmapping_api are set up
// the ws_validate_authorization callback must be set up before executing the make_node_server where make_events_ws_validate_handler is set up
nmos::experimental::authorization_state authorization_state;
if (nmos::experimental::fields::server_authorization(registry_model.settings))
{
Expand Down Expand Up @@ -148,7 +150,7 @@ int main(int argc, char* argv[])
}
#endif

// only implement communication with Authorization server if IS-10/BCP-003-02 is required
// only configure communication with Authorization server if IS-10/BCP-003-02 is required
if (nmos::experimental::fields::server_authorization(registry_model.settings))
{
auto load_ca_certificates = registry_implementation.load_ca_certificates;
Expand Down
6 changes: 3 additions & 3 deletions Development/nmos/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ namespace nmos
// token_endpoint_auth_method [node]: String indicator of the requested authentication method for the token endpoint
// supported methods are client_secret_basic and private_key_jwt, default to client_secret_basic
// when using private_key_jwt, the JWT is created and signed by the node's private key
const web::json::field_as_string_or token_endpoint_auth_method{ U("token_endpoint_auth_method"), U("client_secret_basic")};
const web::json::field_as_string_or token_endpoint_auth_method{ U("token_endpoint_auth_method"), U("client_secret_basic") };

// jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration
// see http_port
Expand All @@ -435,7 +435,7 @@ namespace nmos

// no_trailing_dot_for_authorization_callback_uri [node]: used to specify whether no trailing dot FQDN should be used to construct the URL for the authorization server callbacks
// as it is because not all Authorization server can cope with URL with trailing dot, default to true
const web::json::field_as_bool_or no_trailing_dot_for_authorization_callback_uri{ U("no_trailing_dot_for_authorization_callback_uri"), true};
const web::json::field_as_bool_or no_trailing_dot_for_authorization_callback_uri{ U("no_trailing_dot_for_authorization_callback_uri"), true };

// retry_after [registry, node]: used to specify the HTTP Retry-After header to indicate the number of seconds when the client may retry its request again, default to 5 seconds
// "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss
Expand All @@ -444,7 +444,7 @@ namespace nmos
// Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request.
// If the Resource Server fails to verify a token using all public keys available it MUST reject the token."
// see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys
const web::json::field_as_integer_or service_unavailable_retry_after{ U("service_unavailable_retry_after"), 5};
const web::json::field_as_integer_or service_unavailable_retry_after{ U("service_unavailable_retry_after"), 5 };
}
}
}
Expand Down
21 changes: 12 additions & 9 deletions Documents/Architecture.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Architecture of nmos-cpp

The [nmos](../Development/nmos/) module fundamentally provides three things.
The [nmos](../Development/nmos/) module fundamentally provides three things.

1. A C++ data model for the AMWA IS-04 and IS-05 NMOS resources which represent the logical functionality of a Node, or equally, for the resources of many Nodes held by a Registry.
2. An implementation of each of the REST APIs defined by the AMWA IS-04 and IS-05 NMOS specifications, in terms of the data model.
3. An implementation of the Node and Registry "active behaviours" defined by the specifications.
1. A C++ data model for the AMWA NMOS resources which represent the logical functionality of a Node, or equally, for the resources of many Nodes held by a Registry.
2. An implementation of each of the REST APIs defined by the AMWA NMOS specifications, in terms of the data model.
3. An implementation of the Node and Registry "active behaviours" defined by the specifications.

The module also provides the concept of a server which combines the REST APIs and behaviours into a single object for simplicity.

Expand All @@ -14,12 +14,13 @@ The module also provides the concept of a server which combines the REST APIs an

The top-level data structures for an NMOS Node and Registry are ``nmos::node_model`` and ``nmos::registry_model`` respectively.

A ``node_model`` has three member variables which are containers, of IS-04 resources, IS-05 resources and IS-07 resources, respectively:
A ``node_model`` has four member variables which are containers, of IS-04 resources, IS-05 resources, IS-07 resources and IS-08 resources, respectively:

```C++
nmos::resources node_resources;
nmos::resources connection_resources;
nmos::resources events_resources;
nmos::resources channelmapping_resources;
```

A ``registry_model`` has two containers, this time for the Registry's own Node API "self" resource, and for the resources that have been registered with the Registration API:
Expand Down Expand Up @@ -116,11 +117,13 @@ for (;;)
> [nmos/node_api.cpp](../Development/nmos/node_api.cpp),
> [nmos/connection_api.cpp](../Development/nmos/connection_api.cpp),
> [nmos/events_api.cpp](../Development/nmos/events_api.cpp),
lo-simon marked this conversation as resolved.
Show resolved Hide resolved
> [nmos/channelmapping_api.cpp](../Development/nmos/channelmapping_api.cpp),
> [nmos/registration_api.cpp](../Development/nmos/registration_api.cpp),
> [nmos/query_api.cpp](../Development/nmos/query_api.cpp),
> [nmos/system_api.cpp](../Development/nmos/system_api.cpp)
> [nmos/system_api.cpp](../Development/nmos/system_api.cpp),
> [nmos/authorization_redirect_api.cpp](../Development/nmos/authorization_redirect_api.cpp)

The ``nmos`` module also provides the implementation of each of the REST APIs defined by AMWA IS-04, IS-05, IS-07 and IS-09.
The ``nmos`` module also provides the implementation of each of the REST APIs defined by AMWA IS-04, IS-05, IS-07, IS-08, IS-09 and IS-10.

The C++ REST SDK provides a general purpose HTTP listener, that accepts requests at a particular base URL and passes them to a user-specified request handler for processing.
Therefore the ``nmos`` module implements each API as a request handler which reads and/or writes the relevant parts of the NMOS data model, and provides a convenience function, ``nmos::support_api``, for associating the API request handler with the HTTP listener.
Expand Down Expand Up @@ -167,7 +170,7 @@ The required Node behaviour includes:

The state machine implemented by the ``nmos::node_behaviour_thread`` is shown below:

![NMOS Node Behaviour](images/node-behaviour.png)
![NMOS Node Behaviour](images/node-behaviour.png)

<details>
<summary>More details...</summary>
Expand Down Expand Up @@ -211,7 +214,7 @@ The diagram below shows a sequence of events within and between an **nmos-cpp**
Resource events initiated in a resource-scheduling thread in the Node are propagated via the Registration API to the Registry model.
Events in the Registry model are sent in WebSocket messages to each Client with a matching Query API subscription.

![Sequence Diagram](images/node-registry-sequence.png)
![Sequence Diagram](images/node-registry-sequence.png)

## Servers

Expand Down
114 changes: 114 additions & 0 deletions Documents/Authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Authorization in nmos-cpp

Authorization in nmos-cpp is based on the IS-10 / BCP-003-02 specifications, which are themselves based on OAuth 2.0.

Authorization allows NMOS Nodes and Registries to protect APIs by limiting their access by third-party applications.

Third-party applications might include Broadcast Controllers, which queries the Registry via the IS-04 API and handles connections to nodes via the IS-05 API. NMOS Nodes also act as a third-party to the Registry when performing IS-04 node registrations.

## Overview

A client such as a Broadcast Controller provides credentials to the Authorization Server. The Authorization Server grants the required access token(s) to the Controller for accessing the protected APIs on the NMOS Node. The Node will verify that the access token has the level of the access rights required, and if successfully verified will allow access to the API.

The access token is time-limited, and must be refreshed before it expires. It is recommended to attempt to refresh the token at least 15 seconds before its expiry, or at the half-life of the access token.

To speed up the token validation process, the Node periodically fetches the Authorization Server's public keys, typically once every hour. The public keys allow the Node to perform local token validation without having to bombard the Authorization Server every time an API is accessed.

A similar idea is also applied to how Nodes perform node registration. The Registry obtains the public keys from the Authorization Server, and the Node obtains the registration access token from the Authorization Server. The Node embeds the token into the registration request for Node registration, and Registry heartbeats.

## Client Registration

In this context the term Client is used to refer to clients of the Authorization Server. In this way Broadcast Controllers, Registries and Nodes are all referred to as Clients.

Clients locate the Authorization Server's API endpoints via DNS-SD. The Authorization Server has a well-known endpoint for returning server metadata.

Clients must be registered to the Authorization Server before using the OAuth 2.0 protocol. In the event of successful registration, the Authorization Server will return the `client_id` for a public client and `client_id` and `client_secret` for a confidential client.

However, it is important that a public client which is using the Authorization Code flow register one or more redirect URLs for security purposes. These allow the Authorization Server to ensure any authorization request is genuine and only valid redirect URLs are used for returning authorization codes. While using Client Credentials flow, Private Key JWT can be used in client authentication for extra security.

See the client registration sequence diagram below on how a Node is registered to the Authorization Server.

![Client-Registration](images/Authorization-Client-Registration.png)

## Access Token

There are two ways of requesting access tokens from the Authomrization Server according to the type of authorization grant used. The grant type depends on the location and the nature of the Client involved in obtaining the access token.

A number of grant types are defined in OAuth 2.0, but the IS-10/BCP-003-02 specifications focus on using the following grant types:
- Authorization Code Grant.
- Client Credentials Grant.

### Authorization Code Grant

This is the recommended grant type and should be used if the Client runs within web browser (for instance a Broadcast Controller). An authorization code is returned by the Authorization Server via the Client's redirect URI. The Client can then exchange this code for a time-limited access token, which can be renewed with the refresh token.

For public clients, there is a risk of an attacker hijacking the authorization code. To prevent this Proof Key for Code Exchange (PKCE) is used to further secure the Authorization Code flow.

The PCKE steps are:

Step 1. Create a high entropy cryptographic random string, ``code_verifier``.

Step 2. Convert the ``code_verifier`` to ``code_challenge`` with the following logic:

```
code_challenge=BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
```

Step 3. Includes the ``code_challenge`` and the hashing method used to generate the ``code_challenge`` in the authorization code request.

Step 4. Send the ``code_verifier`` and the ``authorization code`` in exchange for the token. The Authorization Server uses the ``code_verifier`` to recreate the matching ``code_challenge`` to verify the client.

![Authorization-Code-Flow](images/Authorization-Code-Flow.png)

### Client Credentials Grant

This type of authorization is used by Clients to obtain the access token without user interaction. This is used by Nodes with no user interface.

For extra security the Node uses ``Private Key JWT`` to authenticate with the Authorization Server when requesting the access token.

![Client-Credentials-Flow](images/Authorization-Client-Credentials-Flow.png)

## Authorization Server Public Keys

Public keys are used by the Node for validating the access token before allowing access to its protected APIs. The Client must periodically poll the Authorization Server for its public keys, typically once every hour. In the event that the Authorization Server is no longer available, the last fetched public keys will be kept in use until the Authorization Server connection is restored.

Token validation is done by regenerating the matching token signature. This is done by signing the token header and the token payload.

![Public-Keys](images/Authorization-Public-Keys.png)

## Authorization Behaviour

> [nmos/authorization_behaviour.cpp](../../Development/nmos/authorization_behaviour.cpp)

The required Authorization behaviour includes:

- Discovery of the Authorization Server.
- Fetch Authorization Server metadata for Authorization Server endpoints and supported features.
- Authorization Client registration.
- Fetch Authorization Server public keys.
- Fetch Bearer token for accessing protected endpoints.

The state machine implemented by the ```nmos::experimental::authorization_behaviour_thread``` is shown below:

![Authorization-behaviour](images/Authorization-behaviour.png)

## Validating Access Tokens When Public Keys Are Missing

> [nmos/authorization_handlers.cpp](../../Development/nmos/authorization_handlers.cpp)
> [nmos/authorization_behaviour.cpp](../../Development/nmos/authorization_behaviour.cpp)

If no matching public key is available to validate the incoming access token the validation handler will trigger the authorization token issuer thread to fetch and cache the public keys from this token's issuer, which can then be used to validate any token issued by this issuer.

The state machine implemented by the ```nmos::experimental::validate_authorization_handler``` and the ```nmos::experimental::authorization_token_issuer_thread``` are shown below:

![missing-public-keys](images/Authorization-Missing-Public-Keys.png)

In addition, if the Authorization behaviour thread is excluded, the Node/Registry can easily be configured as a headless OAuth 2.0 enabled device.

In this case the access token will be fed in externally via the ```nmos::experimental::get_authorization_bearer_token_handler``` callback and the access token validation will be happening on the ```nmos::experimental::validate_authorization_token_handler``` callback.

## OAuth 2.0 Node Registration Example

The following is an overview of how an OAuth 2.0 Node registers to an OAuth 2.0 enabled Registry.

![Node-Registration](images/Authorization-Node-Registration.png)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit confused by the Authorization Process box at the bottom of the diagram. This contains a Client Credentials Flow box which suggests this is process is infinitely recursive!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to show once the connection is broken between Auth server & the Node, Node will start over again to fetch Auth server metadata.... and Client Credentials flow

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diagram still shows the Device Code Flow grant type in it which should perhaps be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted, I will remove Device Code Flow grant

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Documents/images/Authorization-Code-Flow.png
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the Client Credentials diagram, the Authorization Process box at the bottom of the diagram contains an Authorization Code Flow box suggesting an infinite recursion :|

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Documents/images/Authorization-Public-Keys.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Documents/images/Authorization-behaviour.png
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the "Is required to access protected API?" diamond is asking. Also on "No" it enters the "do authorization_operation" loop which only seems to exit on Failed - is that right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a separate process (like the "start authorization code flow" box) should it perhaps be "start authorization_operation loop"? (same applies I guess to the authorization_operation box at the bottom of the diagram)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading