Kuadrant / kuadrantctl

Kuadrant configuration command line utility
Apache License 2.0
6 stars 12 forks source link

kuadrant authpolicy command: support apikey #50

Closed eguzki closed 7 months ago

eguzki commented 7 months ago

What

The new command kuadrantctl generate kuadrant authpolicy to create kuadrant Auth Policy from OpenAPI Specification (OAS) 3.x powered with kuadrant extensions was introduced in #46

46 implemented the Security Scheme Object type openIdConnect.

This PR implements another type: apiKey

Example

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

kind: AuthPolicy
apiVersion: kuadrant.io/v1beta2
metadata:
  name: petstore
  namespace: petstore
  creationTimestamp: null
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: petstore
    namespace: petstore
  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 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:

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. It can be discussed further and enhanced.

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

Verification Steps

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

Operation Auth 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

Now, let's run local cluster to test the kuadrantctl new command to generate authpolicy.

```yaml cat <petstore-openapi.yaml --- openapi: "3.1.0" info: title: "Pet Store API" version: "1.0.0" x-kuadrant: route: name: "petstore" namespace: "petstore" hostnames: - example.com parentRefs: - name: istio-ingressgateway namespace: istio-system servers: - url: https://example.io/api/v1 paths: /cat: x-kuadrant: backendRefs: - name: petstore port: 80 namespace: petstore 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: # OIDC operationId: "getDog" security: - 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: 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 ```

Replace ${KEYCLOAK_PUBLIC_DOMAIN} with your SSO instance domain

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

Without any credentials, it should return 401 Unauthorized

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_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.

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

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"

without credentials, it should return 401 Unauthorized

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' \
        -d 'client_id=petstore' \
        -d 'scope=openid' \
        -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

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

it should return 200 OK

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

without credentials, it should return 401 Unauthorized

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)

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)

curl -H 'Host: example.com' -i "http://127.0.0.1:9080/api/v1/snake?snake_token=I_LIKE_SNAKES"
codecov-commenter commented 7 months ago

Codecov Report

Attention: 9 lines in your changes are missing coverage. Please review.

Comparison is base (1148fc8) 0.38% compared to head (675c190) 0.38%.

Files Patch % Lines
pkg/utils/maps.go 0.00% 9 Missing :warning:
Additional details and impacted files ```diff @@ Coverage Diff @@ ## httproute-kuadrant-extensions #50 +/- ## ================================================================ - Coverage 0.38% 0.38% -0.01% ================================================================ Files 16 17 +1 Lines 774 783 +9 ================================================================ Hits 3 3 - Misses 771 780 +9 ```

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

jasonmadigan commented 7 months ago

đź‘€

jasonmadigan commented 7 months ago

Works great

eguzki commented 7 months ago

@jasonmadigan completed the verification steps and added some doc

Ready for review!