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

[BUG] Custom Auth caching ignores scopes #1775

Open
2 tasks done
huwcbjones opened this issue Oct 3, 2023 · 2 comments
Open
2 tasks done

[BUG] Custom Auth caching ignores scopes #1775

huwcbjones opened this issue Oct 3, 2023 · 2 comments
Assignees
Labels
Core: Transport Sending data to the tested app Difficulty: Intermediate Requires some experience Priority: Medium Planned for regular releases Type: Bug Errors or unexpected behavior

Comments

@huwcbjones
Copy link
Contributor

huwcbjones commented Oct 3, 2023

Checklist

Describe the bug
When implementing custom auth, I'm using the scopes provided by the security schema to request an access token with the required scopes.
When a request to a different endpoint that requires different scopes is made, schemathesis uses the token with the wrong scopes.

To Reproduce
Steps to reproduce the behavior:

  1. Add a hook implementing custom auth that looks like the sample below
@schemathesis.auth()
class OAuth2Bearer:

    @staticmethod
    def get_scopes_from_ctx(context: AuthContext) -> frozenset[str, ...] | None:
        security = context.operation.definition.get("security", [])
        if not security:
            return None
        scopes = security[0][context.operation.get_security_requirements()[0]]
        if not scopes:
            return None
        return frozenset(scopes)

    def get(self, context: AuthContext) -> str | None:
        if not (scopes := self.get_scopes_from_ctx(context)):
            return None
        token_endpoint = f"{context.operation.base_url}{TOKEN_ENDPOINT}"
        # request token with required scopes for context
        response = requests.post(token_endpoint, data={"scopes": scopes, ...})
        data = response.json()
        assert response.status_code == 200, data
        return data["access_token"]

    def set(self, case: Case, data: str, context: AuthContext) -> None:
        case.headers = case.headers or {}
        if not data:
            return
        case.headers["Authorization"] = f"Bearer {data}"
  1. Run schemathesis on a schema with 2 endpoints that have different security scopes
openapi: 3.0.3
info:
  title: Cats Schema
  description: Cats Schema
  version: 1.0.0
  contact:
    email: info@cats.cat
servers:
  - url: https://cats.cat
tags:
  - name: cat
paths:
  /v1/cats:
    get:
      description: List cats
      operationId: listCats
      tags:
        - cat
      responses:
        "200":
          description: List of cats
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
      security:
        - oAuthBearer: ["list"]
    post:
      description: Create a a cat
      operationId: createCat
      tags:
        - cat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Cat'
      responses:
        "200":
          description: Newly created cat
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Cat'
      security:
        - oAuthBearer: ["create"]

components:
  schemas:
    Cat:
      required:
        - name
      type: object
      properties:
        name:
          type: string
          example: Macavity
  securitySchemes:
    oAuthBearer:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: /oauth/token
          scopes:
            list: "List cats"
            create: "Create cats"

Expected behavior
As documented in the schema, the different endpoints have different security scopes.
Therefore, I'd expect the custom auth implementation to be called again with the different authentication context to request an access token with the right scopes.

Environment (please complete the following information):
N/A, but will provide anyway

  • OS: Linux or macOS
  • Python version: 3.9.2
  • Schemathesis version: 3.19.7
  • Spec version: 3.0.3

Additional context
I've bodged around it by returning the scopes from Auth.get, and comparing the scopes in the Auth.set context to the ones for the token. This obviously means that every request that requires different scopes to the original access token will end up hitting the authentication API, but it means I stop getting 403s from my application server!

    def get(self, context: AuthContext) -> _AuthData:
        # ommitted for brevity
        response = requests.post(...)
        data = response.json()
        assert response.status_code == 200, data
        return scopes, data["access_token"]

    def set(self, case: Case, data: _AuthData, context: AuthContext) -> None:
        case.headers = case.headers or {}
        if not data:
            return
        scopes, access_token = data
        required_scopes = self.get_scopes_from_ctx(context)
        if not required_scopes.issubset(scopes):
            scopes, access_token = self.get(context)
        case.headers["Authorization"] = f"Bearer {access_token}"
@huwcbjones huwcbjones added Status: Needs Triage Requires initial assessment to categorize and prioritize Type: Bug Errors or unexpected behavior labels Oct 3, 2023
@Stranger6667 Stranger6667 removed the Status: Needs Triage Requires initial assessment to categorize and prioritize label Oct 3, 2023
@Stranger6667
Copy link
Member

Thank you so much for opening this and providing such a detailed report! :)
I think we need to support custom keys in the cache implementation + check for the cached value based on the "is subset" relation for scopes.

@huwcbjones
Copy link
Contributor Author

No worries! Your suggestion sounds reasonable 😊
If I get some spare time, I'll see if I can implement that.

@Stranger6667 Stranger6667 added Priority: Medium Planned for regular releases Difficulty: Intermediate Requires some experience Core: Transport Sending data to the tested app labels Oct 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Core: Transport Sending data to the tested app Difficulty: Intermediate Requires some experience Priority: Medium Planned for regular releases Type: Bug Errors or unexpected behavior
Projects
None yet
Development

No branches or pull requests

2 participants