Skip to content

Commit

Permalink
Switch to Bearer auth (#463)
Browse files Browse the repository at this point in the history
* Replace MonitorApiKey key/keyhash auth type with signed JWT implementation
* Add MonitorGenerateKeyRunner to test the generatekey command
Closes #463 
Closes #589
  • Loading branch information
kelltrick authored Aug 13, 2021
1 parent d42c234 commit b914277
Show file tree
Hide file tree
Showing 59 changed files with 2,460 additions and 1,020 deletions.
73 changes: 73 additions & 0 deletions documentation/api-key-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# API Key Format
API Keys or MonitorApiKeys used in `dotnet monitor` are JSON Web Tokens or JWTs as defined by [RFC 7519: JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519).
> **Note:** Because the API Key is a `Bearer` token, it should be treated as a secret and always transmitted over `TLS` or anther protected protocol.
It is possible to make your own API Keys for `dotnet monitor` by following the format as defined below. Although, it is recommended to use the `generatekey` command unless you have a specific reason to make your own key.

## Token Format
To use a JWT for authentication with `dotnet monitor`, the token must follow certain constraints. These constraints will be validated by `dotnet monitor` at configuration load time, authentication time, and authorization time.

For this example, let's consider the API Key given on the [Authentication page](authentication.md). This token consists of 3 parts: Header, Payload, and Signature; explained in detail later. This is the entire portion passed as a `Bearer` type in the `Authorization` HTTP header:
```yaml
eyJhbGciOiJFUffffffffffffCI6IkpXVCJ9.eyJhdWQiOiJodffffffffffffGh1Yi5jb20vZG90bmV0L2RvdG5ldC1tb25pdG9yIiwiaXNzIjoiaHR0cHM6Ly9naXRodWIuY29tL2RvdG5ldC9kb3RuZXQtbW9uaXRvci9nZW5lcmF0ZWtleStNb25pdG9yQXBpS2V5Iiwic3ViIjoiYWU1NDczYjYtOGRhZC00OThkLWI5MTUtNTNiOWM2ODQwMDBlIn0.RZffffffffffff_yIyApvFKcxFpDJ65HJZek1_dt7jCTCMEEEffffffffffffR08OyhZZHs46PopwAsf_6fdTLKB1UGvLr95volwEwIFnHjdvMfTJ9ffffffffffffAU
```
>**Note:** While all values provided in this document are the correct length and format, the raw values have been edited to prevent this public example being used as a dotnet-monitor configuration.
### Header
The header (decoded from the token above) must contain at least 2 elements: `alg` (or [Algorithm](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.1)), and `typ` (or [Type](https://datatracker.ietf.org/doc/html/rfc7519#section-5.1)). `dotnet monitor` expects the `typ` to always be `JWT` for a JSON Web Token. `dotnet monitor` supports 6 `alg` values: `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, and `RS512`.

```json
{
"alg": "ES384",
"typ": "JWT"
}
```
>**Note:** The `alg` requirement is designed to enforce `dotnet monitor` to use public/private key signed tokens. This allows the key that is stored in configuration (as `Authentication__MonitorApiKey__PublicKey`) to only contain public key information and thus does not need to be kept secret.
### Payload
The payload (also decoded from the token above) must contain at least 2 elements: `aud` (or [Audience](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3)), and `sub` (or [Subject](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2)). `dotnet monitor` expects the `aud` to always be `https://github.com/dotnet/dotnet-monitor` which signals that the token is intended for dotnet-monitor. The `sub` field is any non-empty string defined in `Authentication__MonitorApiKey__Subject`, this is used to validate that the token provided is for the expected instance and is user-defined in configuration.

When using the `generatekey` command, the `sub` field will be a randomly-generated `Guid` but the `sub` field may be any non-empty string that matches the configuration. The `iss` (or [Issuer](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1)) field will be set to `https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey` to specify the source of the token, however `dotnet monitor` will accept any `iss` field value, and does not need to be present.
```json
{
"aud": "https://github.com/dotnet/dotnet-monitor",
"iss": "https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey",
"sub": "ae5473b6-8dad-498d-b915-ffffffffffff"
}
```

### Signature
JSON web tokens may be cryptographically signed; `dotnet monitor` requires all tokens to be signed and supports `RSA PKCS1` and `ECDSA` signed tokens, these are tokens with `RS*` and `ES*` as `alg` values. `dotnet monitor` needs the public key portion of this cryptographic material in order to validate the token. See the [Providing a Public Key](#providing-a-public-key) section for information on how to encode a key.

## Providing a Public Key

The public key is provided to `dotnet monitor` as a JSON Web Key or JWK, as defined by [RFC 7517: JSON Web Key (JWK)](https://www.rfc-editor.org/rfc/rfc7517.html). This key must be serialized as JSON and then Base64 URL encoded into a single string.

`dotnet monitor` imposes the following constraints on JWKs:
- The key should **not** have private key data. The key is only used for signature verification, and thus private key parameters are not needed. A warning message will be shown if the private key data is included.
- The `kty` (or [Key Type](https://www.rfc-editor.org/rfc/rfc7517.html#section-4.1)) must be `RSA` for a RSA public key or `EC` for an elliptic-curve public key.

The key used for the token in this example is:

```json
{
"AdditionalData":{},
"Crv":"P-384",
"KeyOps":[],
"Kty":"EC",
"X":"HoffffffffffffuHyjH_57Yf4AkPLEhI5QOTnRugE192Xz_VqcffffffffffffOj",
"X5c":[],
"Y":"JyffffffffffffhzyV-VCMdUttelaY2a8WmileII4MzaYp9j6EffffffffffffFi",
"KeySize":384,
"HasPrivateKey":false,
"CryptoProviderFactory": {
"CryptoProviderCache":{},
"CacheSignatureProviders":true,
"SignatureProviderObjectPoolCacheSize":32
}
}
```
The JWK above is then Base64 URL encoded into the following value which is passed to `dotnet monitor` as `Authentication__MonitorApiKey__PublicKey`
```yaml
eyffffffffffffFsRGF0YSI6e30sIkNydiI6IlAtMzg0IiwiS2V5T3BzIjpbXSwiS3R5IjoiRUMiLCJYIjoiTnhIRnhVZ19QM1dhVUZWVzk0U3dUY3FzVk5zNlFLYjZxc3AzNzVTRmJfQ3QyZHdpN0RWRl8tUTVheERtYlJuWSIsIlg1YyI6W10sIlkiOiJmMXBDdmNoUkVpTWEtc1h6SlZQaS02YmViMHdrZmxfdUZBN0Vka2dwcjF5N251Wmk2cy1NcHl5RzhKdVFSNWZOIiwiS2V5U2l6ZSI6Mzg0LCJIYXNQcml2YXRlS2V5IjpmYWxzZSwiQ3J5cHRvUHJvdmlkZXJGYWN0b3J5Ijp7IkNyeXB0b1Byb3ZpZGVyQ2FjaGUiOnt9LCJDYWNoZVNpZ25hdHVyZVByb3ZpZGVycyI6dHJ1ZSwiU2lnbmF0dXJlUHJvdmlkZXJPYmplY3RQb29sQ2FjaGffffffffffff19
```
85 changes: 37 additions & 48 deletions documentation/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,38 @@ Windows authentication doesn't require explicit configuration and is enabled aut
> **NOTE:** Windows authentication will not be attempted if you are running `dotnet monitor` as an Administrator
## API Key Authentication
An API Key is the recommended authentication mechanism for `dotnet monitor`. To enable it, you will need to generate a secret key, update the configuration of `dotnet monitor`, and then specify the API Key in the Authorization header on all requests to `dotnet monitor`.
An API Key is the recommended authentication mechanism for `dotnet monitor`. API Keys are referred to as `MonitorApiKey` in configuration and source code but we will shorten the term to "API key" in this document. To enable it, you will need to generate a secret token, update the configuration of `dotnet monitor`, and then specify the secret token in the `Authorization` header on all requests to `dotnet monitor`.

> **NOTE:** API Key Authentication should only be used when [TLS is enabled](#) to protect the key while in transit. `dotnet monitor` will emit a warning if authentication is enabled over an insecure transport medium.
### Generating an API Key

The API Key you use to secure `dotnet monitor` should be a 32-byte cryptographically random secret. You can generate a key either using `dotnet monitor` or via your shell. To generate an API Key with `dotnet monitor`, simply invoke the `generatekey` command:
The API Key you use to secure `dotnet monitor` is a secret JWT token, cryptographically signed by a public/private key algorithm. You can generate a key either using `dotnet monitor` or via your shell. To generate an API Key with `dotnet monitor`, simply invoke the `generatekey` command:

```powershell
dotnet monitor generatekey
```

The output from this command will display the API Key formatted as an authentication header along with its hash and associated hashing algorithm. You will need to store the `ApiKeyHash` and `ApiKeyHashType` in the configuration for `dotnet monitor` and use the authorization header value when making requests to the `dotnet monitor` HTTPS endpoint.
The output from this command will display the API key (a bearer JWT token) formatted as an `Authorization` header along with its corresponding configuration for `dotnet monitor`. You will need to store the `Subject` and `PublicKey` in the configuration for `dotnet monitor` and use the `Authorization` header value when making requests to the `dotnet monitor` HTTPS endpoint.

```yaml
Authorization: MonitorApiKey fffffffffffffffffffffffffffffffffffffffffff=
ApiKeyHash: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ApiKeyHashType: SHA256
Generated ApiKey for dotnet-monitor; use the following header for authorization:

Authorization: Bearer eyJhbGciOiJFUffffffffffffCI6IkpXVCJ9.eyJhdWQiOiJodffffffffffffGh1Yi5jb20vZG90bmV0L2RvdG5ldC1tb25pdG9yIiwiaXNzIjoiaHR0cHM6Ly9naXRodWIuY29tL2RvdG5ldC9kb3RuZXQtbW9uaXRvci9nZW5lcmF0ZWtleStNb25pdG9yQXBpS2V5Iiwic3ViIjoiYWU1NDczYjYtOGRhZC00OThkLWI5MTUtNTNiOWM2ODQwMDBlIn0.RZffffffffffff_yIyApvFKcxFpDJ65HJZek1_dt7jCTCMEEEffffffffffffR08OyhZZHs46PopwAsf_6fdTLKB1UGvLr95volwEwIFnHjdvMfTJ9ffffffffffffAU

Settings in Text format:
Subject: ae5473b6-8dad-498d-b915-ffffffffffff
Public Key: eyffffffffffffFsRGF0YSI6e30sIkNydiI6IlAtMzg0IiwiS2V5T3BzIjpbXSwiS3R5IjoiRUMiLCJYIjoiTnhIRnhVZ19QM1dhVUZWVzk0U3dUY3FzVk5zNlFLYjZxc3AzNzVTRmJfQ3QyZHdpN0RWRl8tUTVheERtYlJuWSIsIlg1YyI6W10sIlkiOiJmMXBDdmNoUkVpTWEtc1h6SlZQaS02YmViMHdrZmxfdUZBN0Vka2dwcjF5N251Wmk2cy1NcHl5RzhKdVFSNWZOIiwiS2V5U2l6ZSI6Mzg0LCJIYXNQcml2YXRlS2V5IjpmYWxzZSwiQ3J5cHRvUHJvdmlkZXJGYWN0b3J5Ijp7IkNyeXB0b1Byb3ZpZGVyQ2FjaGUiOnt9LCJDYWNoZVNpZ25hdHVyZVByb3ZpZGVycyI6dHJ1ZSwiU2lnbmF0dXJlUHJvdmlkZXJPYmplY3RQb29sQ2FjaGffffffffffff19
```
>**Note:** While all values provided in this document are the correct length and format, the raw values have been edited to provent this public example being used as a dotnet-monitor configuration.
The API Key is hashed using the SHA256 algorithm when using the `generatekey` command, but other [secure hash implementations](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.hashalgorithm.create?view=net-5.0#System_Security_Cryptography_HashAlgorithm_Create_System_String) (such as SHA384 and SHA512) are also supported; due to collision problems the SHA1 and MD5 algorithms are not permitted.
The `generatekey` command supports 1 parameter `--output`/`-o` to specify the configuration format. By default, `dotnet monitor generatekey` will use the `--output json` format. Currently, the values in the list below are supported values for `--output`.

> NOTE: The generated API Key should be secured at rest. We recommend using a tool such as a password manager to save it.
- `Json` output format will provide a json blob in the correct format to merge with an `appsettings.json` file to specify configuration via json file.
- `Text` output format write the individual parameters in an easily human-readable format.
- `Cmd` output format in environment variables for a `cmd.exe` prompt.
- `PowerShell` output format in environment variables for a `powershell` or `pwsh` prompt.
- `Shell` output format in environment variables for a `bash` shell or another linux shell prompt.

### Configuring dotnet-monitor to use an API Key

Expand All @@ -48,29 +57,31 @@ If you're running on Windows, you can save these settings to `%USERPROFILE%\.dot

```json
{
"ApiAuthentication": {
"ApiKeyHash": "CB233C3BE9F650146CFCA81D7AA608E3A3865D7313016DFA02DAF82A2505C683",
"ApiKeyHashType": "SHA256"
"Authentication": {
"MonitorApiKey": {
"Subject": "ae5473b6-8dad-498d-b915-ffffffffffff",
"PublicKey": "eyffffffffffffFsRGF0YSI6e30sIkNydiI6IlAtMzg0IiwiS2V5T3BzIjpbXSwiS3R5IjoiRUMiLCJYIjoiTnhIRnhVZ19QM1dhVUZWVzk0U3dUY3FzVk5zNlFLYjZxc3AzNzVTRmJfQ3QyZHdpN0RWRl8tUTVheERtYlJuWSIsIlg1YyI6W10sIlkiOiJmMXBDdmNoUkVpTWEtc1h6SlZQaS02YmViMHdrZmxfdUZBN0Vka2dwcjF5N251Wmk2cy1NcHl5RzhKdVFSNWZOIiwiS2V5U2l6ZSI6Mzg0LCJIYXNQcml2YXRlS2V5IjpmYWxzZSwiQ3J5cHRvUHJvdmlkZXJGYWN0b3J5Ijp7IkNyeXB0b1Byb3ZpZGVyQ2FjaGUiOnt9LCJDYWNoZVNpZ25hdHVyZVByb3ZpZGVycyI6dHJ1ZSwiU2lnbmF0dXJlUHJvdmlkZXJPYmplY3RQb29sQ2FjaGffffffffffff19"
}
}
}
```

Alternatively, you can use environment variables to specify the configuration.
Alternatively, you can use environment variables to specify the configuration. Use `dotnet monitor generatekey --output shell` to get the format below.

```sh
DotnetMonitor_ApiAuthentication__ApiKeyHash="CB233C3BE9F650146CFCA81D7AA608E3A3865D7313016DFA02DAF82A2505C683"
DotnetMonitor_ApiAuthentication__ApiKeyHashType="SHA256"
export Authentication__MonitorApiKey__Subject="ae5473b6-8dad-498d-b915-ffffffffffff"
export Authentication__MonitorApiKey__PublicKey="eyffffffffffffFsRGF0YSI6e30sIkNydiI6IlAtMzg0IiwiS2V5T3BzIjpbXSwiS3R5IjoiRUMiLCJYIjoiTnhIRnhVZ19QM1dhVUZWVzk0U3dUY3FzVk5zNlFLYjZxc3AzNzVTRmJfQ3QyZHdpN0RWRl8tUTVheERtYlJuWSIsIlg1YyI6W10sIlkiOiJmMXBDdmNoUkVpTWEtc1h6SlZQaS02YmViMHdrZmxfdUZBN0Vka2dwcjF5N251Wmk2cy1NcHl5RzhKdVFSNWZOIiwiS2V5U2l6ZSI6Mzg0LCJIYXNQcml2YXRlS2V5IjpmYWxzZSwiQ3J5cHRvUHJvdmlkZXJGYWN0b3J5Ijp7IkNyeXB0b1Byb3ZpZGVyQ2FjaGUiOnt9LCJDYWNoZVNpZ25hdHVyZVByb3ZpZGVycyI6dHJ1ZSwiU2lnbmF0dXJlUHJvdmlkZXJPYmplY3RQb29sQ2FjaGffffffffffff19"
```

> **NOTE:** When you use environment variables to configure the API Key hash, you must restart `dotnet monitor` for the changes to take effect.
> **NOTE:** When you use environment variables to configure the API Key, you must restart `dotnet monitor` for the changes to take effect.

### Configuring an API Key in a Kubernetes Cluster
If you're running in Kubernetes, we recommend creating secrets and mounting them into the `dotnet monitor` sidecar container via a deployment manifest. You can use this `kubectl` command to either create or rotate the API Key.

```sh
kubectl create secret generic apikey \
--from-literal=ApiAuthentication__ApiKeyHash=$hash \
--from-literal=ApiAuthentication__ApiKeyHashType=SHA256 \
--from-literal=Authentication__MonitorApiKey__Subject='ae5473b6-8dad-498d-b915-ffffffffffff' \
--from-literal=Authentication__MonitorApiKey__PublicKey='eyffffffffffff...19' \
--dry-run=client -o yaml \
| kubectl apply -f -
```
Expand All @@ -94,52 +105,30 @@ spec:

> **NOTE:** For a complete example of running dotnet-monitor in Kubernetes, see [Running in a Kubernetes Cluster](getting-started.md#running-in-a-kubernetes-cluster) in the Getting Started guide.

### Generate an API Key with PowerShell
```powershell
$rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
$secret = [byte[]]::new(32)
$rng.GetBytes($secret)
$API_KEY = [Convert]::ToBase64String($secret)
"Authorization: MonitorApiKey $API_KEY"
$secretStream = [System.IO.MemoryStream]::new($secret)
$HASHED_KEY = (Get-FileHash -Algorithm SHA256 -InputStream $secretStream).Hash
$rng.Dispose()
Write-Output "ApiKeyHash: $HASHED_KEY"
Write-Output "ApiKeyHashType: SHA256"
```

### Generate an API Key with a Linux shell

```sh
API_KEY=`openssl rand -base64 32`
echo "Authorization: MonitorApiKey $API_KEY"
### Generate API Keys manually

HASHED_KEY=`$API_KEY | base64 -d | sha256sum`
echo "ApiKeyHash: $HASHED_KEY"
echo "ApiKeyHashType: SHA256"
```
API keys for `dotnet monitor` are standard JSON Web Tokens or JWTs and can be generated without the `generatekey` command on `dotnet monitor`. For more details see [Api Key Format](api-key-format.md).

## Authenticating requests
When using Windows Authentication, your browser will automatically handle the Windows authentication challenge. If you are using an API Key, you must specify it via the `Authorization` header.

```sh
curl -H "Authorization: MonitorApiKey fffffffffffffffffffffffffffffffffffffffffff=" https://localhost:52323/processes
curl -H "Authorization: Bearer eyJhbGciOiJFUffffffffffffCI6IkpXVCJ9.eyJhdWQiOiJodffffffffffffGh1Yi5jb20vZG90bmV0L2RvdG5ldC1tb25pdG9yIiwiaXNzIjoiaHR0cHM6Ly9naXRodWIuY29tL2RvdG5ldC9kb3RuZXQtbW9uaXRvci9nZW5lcmF0ZWtleStNb25pdG9yQXBpS2V5Iiwic3ViIjoiYWU1NDczYjYtOGRhZC00OThkLWI5MTUtNTNiOWM2ODQwMDBlIn0.RZffffffffffff_yIyApvFKcxFpDJ65HJZek1_dt7jCTCMEEEffffffffffffR08OyhZZHs46PopwAsf_6fdTLKB1UGvLr95volwEwIFnHjdvMfTJ9ffffffffffffAU" https://localhost:52323/processes
```

If using PowerShell, be sure to use `curl.exe`, as `curl` is an alias for `Invoke-WebRequest` that does not accept the same parameters.
If using PowerShell, you can use `Invoke-WebRequest` but it does not accept the same parameters.

```powershell
curl.exe -H "Authorization: MonitorApiKey fffffffffffffffffffffffffffffffffffffffffff=" https://localhost:52323/processes
(Invoke-WebRequest -Uri https://localhost:52323/processes -Headers @{ 'Authorization' = 'Bearer eyJhbGciOiJFUffffffffffffCI6IkpXVCJ9.eyJhdWQiOiJodffffffffffffGh1Yi5jb20vZG90bmV0L2RvdG5ldC1tb25pdG9yIiwiaXNzIjoiaHR0cHM6Ly9naXRodWIuY29tL2RvdG5ldC9kb3RuZXQtbW9uaXRvci9nZW5lcmF0ZWtleStNb25pdG9yQXBpS2V5Iiwic3ViIjoiYWU1NDczYjYtOGRhZC00OThkLWI5MTUtNTNiOWM2ODQwMDBlIn0.RZffffffffffff_yIyApvFKcxFpDJ65HJZek1_dt7jCTCMEEEffffffffffffR08OyhZZHs46PopwAsf_6fdTLKB1UGvLr95volwEwIFnHjdvMfTJ9ffffffffffffAU' }).Content | ConvertFrom-Json
```

To use Windows authentication with PowerShell, you can specify the `--negotiate` flag
To use Windows authentication with PowerShell, you can specify the `-UseDefaultCredentials` flag for `Invoke-WebRequest` or `--negotiate` for `curl.exe`
```powershell
curl.exe --negotiate https://localhost:52323/processes -u $(whoami)
```


```powershell
(Invoke-WebRequest -Uri https://localhost:52323/processes -UseDefaultCredentials).Content | ConvertFrom-Json
```

## Disabling Authentication

Expand Down
45 changes: 31 additions & 14 deletions documentation/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
}
]
},
"ApiAuthentication": {
"Authentication": {
"default": {},
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/ApiAuthenticationOptions"
"$ref": "#/definitions/AuthenticationOptions"
}
]
},
Expand All @@ -34,7 +34,7 @@
"type": "null"
},
{
"$ref": "#/definitions/CorsConfiguration"
"$ref": "#/definitions/CorsConfigurationOptions"
}
]
},
Expand Down Expand Up @@ -335,28 +335,45 @@
}
}
},
"ApiAuthenticationOptions": {
"AuthenticationOptions": {
"type": "object",
"additionalProperties": false,
"required": [
"ApiKeyHash",
"ApiKeyHashType"
"MonitorApiKey"
],
"properties": {
"ApiKeyHash": {
"MonitorApiKey": {
"description": "The parameters used to validate MonitorApiKey JWT tokens.",
"oneOf": [
{
"$ref": "#/definitions/MonitorApiKeyOptions"
}
]
}
}
},
"MonitorApiKeyOptions": {
"type": "object",
"additionalProperties": false,
"required": [
"Subject",
"PublicKey"
],
"properties": {
"Subject": {
"type": "string",
"description": "API key in hashed form. Each byte should be two hexadecimal-based digits.",
"minLength": 64,
"pattern": "[0-9a-fA-F]+"
"description": "The value of the 'sub' or Subject field in the JWT (JSON Web Token).",
"minLength": 1
},
"ApiKeyHashType": {
"PublicKey": {
"type": "string",
"description": "Hash algorithm used to compute ApiKeyHash, typically 'SHA256'. 'SHA1' and 'MD5' are not allowed.",
"minLength": 1
"description": "The public key used to sign the JWT (JSON Web Token) used for authentication. This field is a JSON Web Key serialized as JSON encoded with base64Url encoding. The JWK must have a kty field of RSA or EC and should not have the private key information.",
"minLength": 1,
"pattern": "[0-9a-zA-Z_-]+"
}
}
},
"CorsConfiguration": {
"CorsConfigurationOptions": {
"type": "object",
"additionalProperties": false,
"required": [
Expand Down
Loading

0 comments on commit b914277

Please sign in to comment.