Skip to content

Commit

Permalink
kuadrant authpolicy command: doc
Browse files Browse the repository at this point in the history
  • Loading branch information
eguzki committed Nov 28, 2023
1 parent 95f56b3 commit 546d66f
Showing 1 changed file with 270 additions and 22 deletions.
292 changes: 270 additions & 22 deletions doc/generate-kuadrant-auth-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ OpenAPI document resource can be provided by one of the following channels:
* URL format (supported schemes are HTTP and HTTPS). The CLI will try to download from the given address.
* Read from stdin standard input stream.

#### openIdConnect type
This initial version of the command only generates AuhPolicy when there is at least one security requirement referencing the
[Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object) which type is `openIdConnect`.
OpenAPI [Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object) types

### Description
| Types | Implemented |
| --- | --- |
| `openIdConnect` | **YES** |
| `apiKey` | **YES** |
| `http` | NO |
| `oauth2` | NO |

### `openIdConnect` Type Description

The following OAS example has one protected endpoint `GET /dog` with OIDC sec scheme.
The following OAS example has one protected endpoint `GET /dog` with `openIdConnect` security scheme type.

```yaml
paths:
Expand Down Expand Up @@ -61,7 +66,7 @@ spec:
method: GET
rules:
authentication:
getDog:
getDog_securedDog:
credentials: {}
jwt:
issuerUrl: https://example.com/.well-known/openid-configuration
Expand All @@ -73,6 +78,91 @@ spec:
method: GET
```
### `apiKey` Type Description

The following OAS example has one protected endpoint `GET /dog` with `apiKey` security scheme type.

```yaml
paths:
/dog:
get:
operationId: "getDog"
security:
- securedDog: []
responses:
405:
description: "invalid input"
components:
securitySchemes:
securedDog:
type: apiKey
name: dog_token
in: query
```

Running the command

```
kuadrantctl generate kuadrant authpolicy --oas ./petstore-openapi.yaml | yq -P
```
The generated authpolicy (only relevan fields shown here):
```yaml
kind: AuthPolicy
apiVersion: kuadrant.io/v1beta2
metadata:
name: petstore
namespace: petstore
creationTimestamp: null
spec:
routeSelectors:
- matches:
- path:
type: Exact
value: /dog
method: GET
rules:
authentication:
getDog_securedDog:
credentials:
queryString:
name: dog_token
apiKey:
selector:
matchLabels:
kuadrant.io/apikeys-by: securedDog
routeSelectors:
- matches:
- path:
type: Exact
value: /dog
method: GET
```

In this particular example, the endpoint `GET /dog` will be protected.
The token needs to be in the query string of the request included in a parameter named `dog_token`.
Kuadrant will validate received tokens against tokens found in kubernetes secrets with label `kuadrant.io/apikeys-by: ${sec scheme name}`.
In this particular example the label selector will be: `kuadrant.io/apikeys-by: securedDog`.

Like the following example:

```yaml
apiVersion: v1
kind: Secret
metadata:
name: api-key-1
labels:
authorino.kuadrant.io/managed-by: authorino
kuadrant.io/apikeys-by: securedDog
stringData:
api_key: MYSECRETTOKENVALUE
type: Opaque
```
> **Note**: Kuadrant validates tokens against api keys found in secrets. The label selector format `kuadrant.io/apikeys-by: ${sec scheme name}` is arbitrary and designed for this CLI command.

For more information about Kuadrant auth based on api key: https://docs.kuadrant.io/authorino/docs/user-guides/api-key-authentication/

### Usage

```shell
Expand All @@ -93,6 +183,16 @@ Global Flags:

### User Guide

The verification steps will lead you to the process of deploying and testing the following api with
endpoints protected using different security schemes:

| Operation | Security Scheme |
| --- | --- |
| `GET /api/v1/cat` | public (not auth) |
| `POST /api/v1/cat` | ApiKey in header |
| `GET /api/v1/dog` | OpenIdConnect |
| `GET /api/v1/snake` | OpenIdConnect **OR** ApiKey in query string |

* [Optional] Setup SSO service supporting OIDC. For this example, we will be using [keycloak](https://www.keycloak.org).
* Create a new realm `petstore`
* Create a client `petstore`. In the Client Protocol field, select `openid-connect`.
Expand Down Expand Up @@ -143,7 +243,7 @@ kubectl apply -n petstore -f examples/petstore/petstore.yaml
```yaml
cat <<EOF >petstore-openapi.yaml
---
openapi: "3.0.3"
openapi: "3.1.0"
info:
title: "Pet Store API"
version: "1.0.0"
Expand All @@ -165,69 +265,189 @@ paths:
- name: petstore
port: 80
namespace: petstore
get: # public (not auth)
get: # No sec requirements
operationId: "getCat"
responses:
405:
description: "invalid input"
post: # API key
operationId: "postCat"
security:
- cat_api_key: []
responses:
405:
description: "invalid input"
/dog:
x-kuadrant:
backendRefs:
- name: petstore
port: 80
namespace: petstore
get: # secured
get: # OIDC
operationId: "getDog"
security:
- openIdConnect: []
- oidc:
- read:dogs
responses:
405:
description: "invalid input"
/snake:
x-kuadrant:
backendRefs:
- name: petstore
port: 80
namespace: petstore
get: # OIDC or API key
operationId: "getSnake"
security:
- oidc: ["read:snakes"]
- snakes_api_key: []
responses:
405:
description: "invalid input"
components:
securitySchemes:
openIdConnect:
cat_api_key:
type: apiKey
name: api_key
in: header
oidc:
type: openIdConnect
openIdConnectUrl: https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore
snakes_api_key:
type: apiKey
name: snake_token
in: query
EOF
```

</details>

> Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain

| Operation | Applied config |
| --- | --- |
| `GET /api/v1/cat` | public (not auth) |
| `GET /api/v1/dog` | OIDC authenticatred |
* Create an API key only valid for `POST /api/v1/cat` endpoint
```yaml
kubectl apply -f -<<EOF
apiVersion: v1
kind: Secret
metadata:
name: cat-api-key-1
namespace: petstore
labels:
authorino.kuadrant.io/managed-by: authorino
kuadrant.io/apikeys-by: cat_api_key
stringData:
api_key: I_LIKE_CATS
type: Opaque
EOF
```
> **Note**: the label's value of `kuadrant.io/apikeys-by: cat_api_key` is the name of the sec scheme of the OpenAPI spec.

* Create an API key only valid for `GET /api/v1/snake` endpoint

```yaml
kubectl apply -f -<<EOF
apiVersion: v1
kind: Secret
metadata:
name: snake-api-key-1
namespace: petstore
labels:
authorino.kuadrant.io/managed-by: authorino
kuadrant.io/apikeys-by: snakes_api_key
stringData:
api_key: I_LIKE_SNAKES
type: Opaque
EOF
```

> **Note**: the label's value of `kuadrant.io/apikeys-by: snakes_api_key` is the name of the sec scheme of the OpenAPI spec.

* Create the HTTPRoute using the CLI

```bash
bin/kuadrantctl generate gatewayapi httproute --oas petstore-openapi.yaml | kubectl apply -n petstore -f -
```

* Create Kuadrant's Auth Policy

```bash
bin/kuadrantctl generate kuadrant authpolicy --oas petstore-openapi.yaml | kubectl apply -n petstore -f -
```

Now, we are ready to test OpenAPI endpoints :exclamation:

- `GET /api/v1/cat` -> It's a public endpoint, hence should return 200 Ok

```bash
curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/cat"
```
- `GET /api/v1/dog` -> It's a secured endpoint, hence, without credentials, it should return 401

- `POST /api/v1/cat` -> It's a protected endpoint with apikey

Without any credentials, it should return `401 Unauthorized`

```bash
curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/dog"
curl -H "Host: example.com" -X POST -i "http://127.0.0.1:9080/api/v1/cat"
```

```
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="getDog"
x-ext-auth-reason: credential not found
date: Tue, 28 Nov 2023 09:38:26 GMT
www-authenticate: Bearer realm="getDog_oidc"
www-authenticate: Bearer realm="getSnake_oidc"
www-authenticate: snake_token realm="getSnake_snakes_api_key"
www-authenticate: api_key realm="postCat_cat_api_key"
x-ext-auth-reason: {"postCat_cat_api_key":"credential not found"}
date: Tue, 28 Nov 2023 22:28:44 GMT
server: istio-envoy
content-length: 0
```
The *reason* headers tell that `credential not found`.
Credentials satisfying `postCat_cat_api_key` authentication is needed.
According to the OpenAPI spec, it should be a header named `api_key`.
What if we try a wrong token? one token assigned to other endpoint,
i.e. `I_LIKE_SNAKES` instead of the valid one `I_LIKE_CATS`. It should return `401 Unauthorized`.
```bash
curl -H "Host: example.com" -H "api_key: I_LIKE_SNAKES" -X POST -i "http://127.0.0.1:9080/api/v1/cat"
```

```
TTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="getDog_oidc"
www-authenticate: Bearer realm="getSnake_oidc"
www-authenticate: snake_token realm="getSnake_snakes_api_key"
www-authenticate: api_key realm="postCat_cat_api_key"
x-ext-auth-reason: {"postCat_cat_api_key":"the API Key provided is invalid"}
date: Tue, 28 Nov 2023 22:32:55 GMT
server: istio-envoy
content-length: 0
```
- Get authentication token. This example is using Direct Access Grants oauth2 grant type (also known as Client Credentials grant type). When configuring the Keycloak (OIDC provider) client settings, we enabled Direct Access Grants to enable this procedure. We will be authenticating as `bob` user with `p` password. We previously created `bob` user in Keycloak in the `petstore` realm.

The *reason* headers tell that `the API Key provided is invalid`.
Using valid token (from the secret `cat-api-key-1` assigned to `POST /api/v1/cats`)
in the `api_key` header should return 200 Ok

```
curl -H "Host: example.com" -H "api_key: I_LIKE_CATS" -X POST -i "http://127.0.0.1:9080/api/v1/cat"
```

- `GET /api/v1/dog` -> It's a protected endpoint with oidc (assigned to our keycloak instance and `petstore` realm)

without credentials, it should return `401 Unauthorized`

```bash
curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/dog"
```

To get the authentication token, this example is using Direct Access Grants oauth2 grant type
(also known as Client Credentials grant type). When configuring the Keycloak (OIDC provider) client
settings, we enabled Direct Access Grants to enable this procedure.
We will be authenticating as `bob` user with `p` password.
We previously created `bob` user in Keycloak in the `petstore` realm.

```
export ACCESS_TOKEN=$(curl -k -H "Content-Type: application/x-www-form-urlencoded" \
-d 'grant_type=password' \
Expand All @@ -236,14 +456,42 @@ export ACCESS_TOKEN=$(curl -k -H "Content-Type: application/x-www-form-urlencode
-d 'username=bob' \
-d 'password=p' "https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore/protocol/openid-connect/token" | jq -r '.access_token')
```

> Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain

With the access token in place, let's try to get those puppies

```bash
curl -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Host: example.com' http://127.0.0.1:9080/api/v1/dog -i
```
should return 200 Ok

it should return 200 OK

- `GET /api/v1/snake` -> It's a protected endpoint with oidc (assigned to our keycloak instance and `petstore` realm) **OR** with apiKey

This example is to show that multiple security requirements (with *OR* semantics) can be specified
for an OpenAPI operation.

Without credentials, it should return `401 Unauthorized`

```bash
curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/snake"
```

With the access token in place, it should return 200 OK (unless the token has expired).

```bash
curl -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Host: example.com' http://127.0.0.1:9080/api/v1/snake -i
```

With apiKey it should also work. According to the OpenAPI spec security scheme,
it should be a query string named `snake_token` and the token needs to be valid token
(from the secret `snake-api-key-1` assigned to `GET /api/v1/snake`)

```bash
curl -H 'Host: example.com' -i "http://127.0.0.1:9080/api/v1/snake?snake_token=I_LIKE_SNAKES"
```

* Clean environment
```bash
Expand Down

0 comments on commit 546d66f

Please sign in to comment.