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

support custom swagger config and oauth redirects #262

Merged
merged 2 commits into from
Dec 7, 2024
Merged

Conversation

mmerickel
Copy link
Member

@mmerickel mmerickel commented Dec 6, 2024

These are all explorer features:

  • Added a new ui_config parameter to add extra options to the SwaggerUI constructor. Obviously you can't configure everything but it allows some support.
  • Added a new enable_oauth_redirect boolean which will enable and start serving the oauth2-redirect.html which is required for a proper oauth2 flow through the swagger UI.
  • Added a new oauth_config which will invoke the ui.initOAuth API on the SwaggerUI which is necessary to enable things like the PKCE challenge flow that is managed automatically by the oauth2-redirect HTML.

Fixes #261.

This enables the following dummy example to work against Azure AD:

import jwt
import requests
from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPForbidden
from pyramid.view import view_config
from waitress import serve

tenant_id = '...'
client_id = '...'
scopes = [
    f'{tenant_id}/.default'
]
oidc_config_url = f'https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration'

oidc_config = requests.get(oidc_config_url).json()
jwks_client = jwt.PyJWKClient(oidc_config['jwks_uri'])

@view_config(route_name='/userinfo', renderer='json')
def userinfo(request):
    if request.authorization.authtype != 'Bearer':
        raise HTTPForbidden
    token = request.authorization.params
    signing_key = jwks_client.get_signing_key_from_jwt(token)
    signing_algorithms = oidc_config['id_token_signing_alg_values_supported']
    claims = jwt.decode(token, key=signing_key, algorithms=signing_algorithms, audience=client_id)
    return claims


with Configurator() as config:
    config.include('pyramid_openapi3')

    config.pyramid_openapi3_spec('openapi.yaml', route='openapi.yaml')
    config.pyramid_openapi3_add_explorer(
        oauth_config={
            'clientId': client_id,
            'scopes': ' '.join(scopes),
            'usePkceWithAuthorizationCodeGrant': True,
        },
    )

    config.add_route('/userinfo', '/userinfo')

    config.scan(__name__)
    app = config.make_wsgi_app()

serve(app, listen='localhost:8000')

Example openapi.yaml:

openapi: 3.1.0
info:
  title: My Awesome API
  version: 0.1.0
paths:
  /userinfo:
    get:
      summary: Userinfo
      responses:
        "200":
          description: Successful Response
          content:
            application/json:
              schema: {}
      security:
        - azure: []
components:
  securitySchemes:
    azure:
      type: oauth2
      flows:
        authorizationCode:
          scopes:
            <client_id>/.default: 'Required scope that indicates to Azure that this app is logging in. Without it the audience will be the Microsoft Graph API instead of us as the client.'
          authorizationUrl: https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/authorize
          tokenUrl: https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token

@mmerickel mmerickel force-pushed the swagger-config branch 10 times, most recently from dfd0db9 to b4db9bc Compare December 6, 2024 04:02
@zupo
Copy link
Collaborator

zupo commented Dec 6, 2024

Is it possible to get an additional test that showcases the requests workflow against azure AD or similar, with mocked responses?

I don't have much experience with oauth2 and a would be afraid of breaking it in future updates without a more high-level testsuite.

Not a blocker though.

@mmerickel
Copy link
Member Author

mmerickel commented Dec 6, 2024

@zupo Hey I'm interested but not on the same page:

  • I don't really know how to mock responses well - is there an existing pattern for what you're asking in the codebase?
  • I actually couldn't figure out how to run things on my macos machine because I don't have nix setup etc so I was even force-pushing here to run the tests so adding more is a bit worrisome to me :-)

Ultimately this flow is quite simple (as simple as any standard oauth2 flow) and is baked into the swagger ui.

  1. Click "authorize" in swagger.
  2. Select to login with the oauth security scheme that you setup - clientId and scopes are prepopulated using the oauth_config setting.
  3. New tab opens and redirects to azure where you login.
  4. Redirect sends you back to this oauth2-redirect.html which processes the data from microsoft.
  5. The oauth2-redirect.html sets some local state and closes the tab.
  6. You're back on the original swagger page and are logged in.
  7. You can now test api requests and the negotiated token is sent in the requests as Authorization: Bearer <token> to your api where you can see my example /userinfo view is validating it and pulling out the claims.

I'm trying to think about what aspects of this could have further tests and mocked responses and struggling. Thoughts?

@zupo
Copy link
Collaborator

zupo commented Dec 6, 2024

Hey @mmerickel!

  1. Can you share a bit more what you tried to get tests running on your local? It should be something along the lines of:
$ poetry install
$ poetry shell
$ make unit
# or if make unit fails, just: pytest pyramid_openapi3
  1. I usually mock responses with https://pypi.org/project/responses/, but this is certainly not the only way.

  2. Can you share a test account on azure and similar so that I can try the oauth2 flow you described above? I could then help write the test cases.

@mmerickel
Copy link
Member Author

mmerickel commented Dec 6, 2024

Ok it took me a minute to figure out but the issue was poetry created a 3.13 virtualenv which isn't compatible with the makefile hardcoding PYTHON=python3.12. The following works:

  • poetry env use 3.12
  • poetry install
  • poetry run make unit

Thank you for helping with that, I was distracted by all the nix stuff and the almost-empty poetry.toml that I wasn't familiar with. I can run the tests now.

@mmerickel
Copy link
Member Author

Basically 100% of this flow is in javascript so I'm trying to figure out what responses to mock. None of it hits the python backend other than what's tested already - loading the swagger html and the oauth2-redirect html as basically static pages.

Copy link
Collaborator

@zupo zupo left a comment

Choose a reason for hiding this comment

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

Generally mergeable, have some minor (potentially stupid) questions.

@zupo zupo merged commit 5a373cf into main Dec 7, 2024
3 checks passed
@mmerickel mmerickel deleted the swagger-config branch December 8, 2024 06:20
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.

support swagger ui config json
2 participants