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

kuadrant authpolicy command: support apikey #50

Merged
merged 4 commits into from
Nov 29, 2023

Conversation

eguzki
Copy link
Collaborator

@eguzki eguzki commented Nov 28, 2023

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
  • [Optional] Setup SSO service supporting OIDC. For this example, we will be using keycloak.
    • Create a new realm petstore
    • Create a client petstore. In the Client Protocol field, select openid-connect.
    • Configure client settings. Access Type to public. Direct Access Grants Enabled to ON (for this example password will be used directly to generate the token).
    • Add a user to the realm
      • Click the Users menu on the left side of the window. Click Add user.
      • Type the username bob, set the Email Verified switch to ON, and click Save.
      • On the Credentials tab, set the password p. Enter the password in both the fields, set the Temporary switch to OFF to avoid the password reset at the next login, and click Set Password.

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

  • Clone the repo and checkout to the current PR branch authpolicy-api-key
  • Setup cluster, istio and Gateway API CRDs
make local-setup
  • Build and install CLI in bin/kuadrantctl path
make install
  • Install Kuadrant service protection. The CLI can be used to install kuadrant v0.4.1
bin/kuadrantctl install 
  • Deploy petstore backend API
kubectl create namespace petstore
kubectl apply -n petstore -f examples/petstore/petstore.yaml
  • Let's create Petstore's OpenAPI spec
cat <<EOF >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

  • Create an API key only valid for POST /api/v1/cat endpoint
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
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
bin/kuadrantctl generate gatewayapi httproute --oas petstore-openapi.yaml | kubectl apply -n petstore -f -
  • Create Kuadrant's Auth Policy
bin/kuadrantctl generate kuadrant authpolicy --oas petstore-openapi.yaml | kubectl apply -n petstore -f -

Now, we are ready to test OpenAPI endpoints ❗

  • GET /api/v1/cat -> It's a public endpoint, hence should return 200 Ok
curl  -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/cat"
  • POST /api/v1/cat -> It's a protected endpoint with apikey

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"
  • 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

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

  • 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 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"
  • Clean environment
make local-cleanup  

@codecov-commenter
Copy link

codecov-commenter commented Nov 28, 2023

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 ⚠️
Additional details and impacted files
@@                       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     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@jasonmadigan
Copy link
Member

👀

@jasonmadigan
Copy link
Member

Works great

Base automatically changed from authpolicy to httproute-kuadrant-extensions November 28, 2023 22:13
@eguzki eguzki force-pushed the authpolicy-api-key branch from 8957a5a to 546d66f Compare November 28, 2023 23:21
@eguzki eguzki marked this pull request as ready for review November 28, 2023 23:23
@eguzki
Copy link
Collaborator Author

eguzki commented Nov 28, 2023

@jasonmadigan completed the verification steps and added some doc

Ready for review!


Like the following example:

```yaml
Copy link
Member

Choose a reason for hiding this comment

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

one for later maybe, but do you think it'd be nice to generate an example secret too? not a big deal, just something I thought of while using

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

you mean that the command kuadrantctl generate kuadrant authpolicy generates the authpolicy and additionally some secret? That could be done. Or write in stderr (stdout will likely be used piped with kubectl) a example secret too.

Copy link
Member

Choose a reason for hiding this comment

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

exactly yeah - could be handy? not urgent, but nice to have

Copy link
Collaborator Author

@eguzki eguzki Nov 30, 2023

Choose a reason for hiding this comment

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

This sounds good to you?

❯ bin/kuadrantctl generate kuadrant authpolicy --oas examples/oas3/petstore-multiple-sec-requirements.yaml 1>/dev/null
======================================================================================================
POST /v1/cat endpoint is protected with ApiKey. Consider creating secrets with valid tokens
---
apiVersion: v1
kind: Secret
metadata:
  creationTimestamp: null
  labels:
    authorino.kuadrant.io/managed-by: authorino
    kuadrant.io/apikeys-by: cat_api_key
  name: cat_api_key
stringData:
  api_key: MY_SECRET_TOKEN_VALUE
type: Opaque

======================================================================================================
GET /v1/snake endpoint is protected with ApiKey. Consider creating secrets with valid tokens
---
apiVersion: v1
kind: Secret
metadata:
  creationTimestamp: null
  labels:
    authorino.kuadrant.io/managed-by: authorino
    kuadrant.io/apikeys-by: snakes_api_key
  name: snakes_api_key
stringData:
  api_key: MY_SECRET_TOKEN_VALUE
type: Opaque

Note: stdout has been redirected to /dev/null

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

#51

@eguzki eguzki merged commit 6405744 into httproute-kuadrant-extensions Nov 29, 2023
5 checks passed
@eguzki eguzki deleted the authpolicy-api-key branch November 29, 2023 09:37
@eguzki eguzki mentioned this pull request Nov 29, 2023
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants