tfranzel / drf-spectacular

Sane and flexible OpenAPI 3 schema generation for Django REST framework.
https://drf-spectacular.readthedocs.io
BSD 3-Clause "New" or "Revised" License
2.28k stars 254 forks source link

Authenticating to OIDC server is not redirecting back to the page , but keeps staying on the tab that opens when authorizing. #1196

Open KlodianMaloku-Rt opened 5 months ago

KlodianMaloku-Rt commented 5 months ago

Describe the bug When trying to authorize , UI opens a new tab that redirects to the OIDC server ( in my case keycloak ) . After authenticating to idp the new tab is not closed but stays open and swagger is not authorized. I have to add a interceptor to use that token that is in url of the redirected page. To Reproduce heare are my configs:

  1. in settings
    
    INSTALLED_APPS = [
    .....
    'call.integrations.swagger',
    ....
    ]

AUTHENTICATION_BACKENDS = ( 'social_core.backends.okta_openidconnect.OktaOpenIdConnect', 'django.contrib.auth.backends.ModelBackend', )

REST_FRAMEWORK = { ......... 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', }

SPECTACULAR_SETTINGS = { 'TITLE': 'Console API', 'DESCRIPTION': 'Console API', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, "SWAGGER_UI_SETTINGS": { "persistAuthorization": True, "withCredentials": True }, 'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], 'SERVE_AUTHENTICATION': [], 'SCHEMA_PATH_PREFIX': r'\/(?:api\/)?callcenter(?:\/public\/v\d+)?(?:\/config)?(?:\/workspace\/{workspace_id})?\/?', }


2.  'call.integrations.swagger',

from drf_spectacular.extensions import OpenApiAuthenticationExtension

class BearerTokenAuthenticationSchema(OpenApiAuthenticationExtension): """ Swagger schema extension to add oauth2 authentication and targeting our authentication class. """ target_class = "call.auth.authentication.UserTokenAuthentication"

def get_security_definition(self, auto_schema):
    from call.core.models import Account
    account = Account.objects.filter(idp__isnull=False).first()

    return {
        'type': 'oauth2',
        'description': 'Bearer token authentication, using keycloak',
        'flows': {
            'implicit': {
                'authorizationUrl': f'{account.idp.idp_hostname}/realms/{account.keycloak_realm}/protocol/'
                                    'openid-connect/auth',
                'tokenUrl': f'{account.idp.idp_hostname}/realms/{account.keycloak_realm}/protocol/openid-connect/token',
                'refreshUrl': f'{account.idp.idp_hostname}/realms/{account.keycloak_realm}/protocol/openid-connect/token',
                'scopes': {
                    'openid': 'openid',
                    'profile': 'profile'
                },
            }
        },
    }
3. script in ui


**Expected behavior**
I was expecting that when authenticating swagger  a new tab opens , and after authenticating to keycloak, this tab closes and i am turned back to the first tab. When turning back to the first tab the pop up of authentication shows that i am authenticated.
tfranzel commented 5 months ago

Hi, I have never used this particular ouath2 scheme. It seems strange that the window will not close. Extracting the access_token from the the popup response seems to me like a missing functionality in SwaggerUI itself (for that auth flow). The auth window not closing certainly points in that direction.

accessToken = params.get('access_token');

I think this should be picked up by SwaggeUI. Maybe it is already saved internally, just not used. Have a look at our injectAuthCredentials() ->authDef.schema

Also notice that we have a setting for initOAuthParams: SWAGGER_UI_OAUTH2_CONFIG

Beware that we also have a interceptor which you are overriding now: https://github.com/tfranzel/drf-spectacular/blob/972141ba71cf3fd3ef37958c3a5f0f38b5d78464/drf_spectacular/templates/drf_spectacular/swagger_ui.js#L100C7-L100C25

We just merged #1191, because we missed reloading the schema after successful oauth2 authentication. However, I think this issue seems one step before that. It is worth a try though.

Let me know what works

tfranzel commented 5 months ago

Might https://github.com/tfranzel/drf-spectacular/pull/1142 be the source of the problem?

Apparently the obtained credentials cannot flow back to the origin but are are kind of past that point already.

KlodianMaloku-Rt commented 5 months ago

Maybe this will fix the problem. When is this going to merge? Should I use drf-spectaculuar-sidecar alone or together with drf-spectacular to get the latest version after the fix is merged?

tfranzel commented 5 months ago

you don't need to use drf-spectaculuar-sidecar. Also there are no modifications there, just "cached" assets. This would only be beneficial for serving the oauth2-redirect.htmlfrom your origin, nothing else imho.

I am reviewing #1142 atm

Since I cannot rebuild your setup exactly, it would be helpful if you could find out where it hangs for you.

ftsell commented 5 months ago

I can at least confirm that the bug I'm fixing in #1142 shows the same behavior as described here. The tab opened by Swagger-UI stays open (and blank) while complaining in its javascript console that window.opener is null.

I don't know anything about addin interceptors to swagger itself though 😅

KlodianMaloku-Rt commented 5 months ago

Hello, I tried to extend the view class includeing the header to response. Also removed my template and let drf-spectacular use the default template and it didnt work. Here is my impl:

from django.conf import settings
from drf_spectacular.utils import extend_schema
from drf_spectacular.views import SpectacularSwaggerView

class CallSwaggerView(SpectacularSwaggerView):

    @extend_schema(exclude=True)
    def get(self, request, *args, **kwargs):
        response = super().get(request, *args, **kwargs)
        response.data['client_id'] = settings.SWAGGER_KCLOAK_CLIENT_ID
        response.data['idpHint'] = ''
        response.headers = {
            "Cross-Origin-Opener-Policy": "unsafe-none",
        }
        return response

class Provider1SwaggerView(CallSwaggerView):

    @extend_schema(exclude=True)
    def get(self, request, *args, **kwargs):
        response = super().get(request, *args, **kwargs)
        return response

class Provider2SwaggerView(CallSwaggerView):

    @extend_schema(exclude=True)
    def get(self, request, *args, **kwargs):
        from smartcall.core.models import Account
        from call.utils.platform_utils import ProviderNames
        account_2 = Account.objects.filter(idp__platform=ProviderNames.Provider2).first()
        response = super().get(request, *args, **kwargs)
        response.data['idpHint'] = account_2.provider_id
        return response

And the url conf is this one:

swagger_urlpatterns = [
    # schema view
    re_path(r'^call/schema/', Provider1SwaggerView.as_view(), name='schema'),
    # Provider1 UI
    re_path(
        r'^call/api-provider1/',
        Provider1SwaggerView.as_view(
            url_name='schema',
            template_name='swagger.html'
        ),
        name='swagger-ui-provider1'
    ),
    # Provider2 UI
    re_path(
        r'^call/api-provider2/',
        Provider2SwaggerView.as_view(
            url_name='schema',
            template_name='swagger.html'
        ),
        name='swagger-ui-provider2'
    ),
    # redoc UI
    re_path(r'^call/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
] if settings.SHOW_SWAGGER else []

I have two url because I have different auth schema for different providers.

KlodianMaloku-Rt commented 5 months ago

@ftsell in my case the tab stays open but not blank. After authenticating to keycloak it redirects back to my swagger url ( in the same tab ) but as not authenticated. Thats it why i have to use that interceptor.

KlodianMaloku-Rt commented 5 months ago

@tfranzel Maybe can help, after authenticating to the new tab it redirects back with the token in url: https:///oauth2-redirect.html#state=&session_state=&access_token=&token_type=Bearer&expires_in=900