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

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 this issue Mar 7, 2024 · 8 comments

Comments

@KlodianMaloku-Rt
Copy link

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\})?\/?',
}
  1. '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'
                    },
                }
            },
        }
  1. script in ui
  <script>
        
        document.addEventListener("DOMContentLoaded", function() {
            //Try getting the access token from url of the auth2-redirect.html page
            let accessToken = null;
            try {
                const currentUrl = window.location.href;
                const url = new URL(currentUrl);
                const params = new URLSearchParams(url.hash.replace('#', '?'));
                accessToken = params.get('access_token');
            } catch (error) {
                console.error(error);
            }
            
            const ui = SwaggerUIBundle({
                url: '/callcenter/schema/',
                dom_id: '#swagger-ui',
                presets: [
                    SwaggerUIBundle.presets.apis,
                    SwaggerUIBundle.SwaggerUIStandalonePreset
                ],
                plugins: [
                    SwaggerUIBundle.plugins.DownloadUrl
                ],
                layout: 'BaseLayout',
                requestInterceptor: (request) => {
                if (accessToken) {
                    request.headers['Authorization'] = `Bearer ${accessToken}`;
                    }
                    return request;
                },
            });
            const initOAuthParams= {
                enable: true,
                clientId: '{{ client_id }}',
                appName: 'Swagger API Documentation',
                scopeSeparator: ' ',
                scopes: 'openid profile',
                useBasicAuthenticationWithAccessCodeGrant: true,
                usePkceWithAuthorizationCodeGrant: true,
                additionalQueryStringParams:{
                    'kc_idp_hint': '{{ idpHint }}'
                }
            }
    
            ui.initOAuth(initOAuthParams);

        });
        
    </script>

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
Copy link
Owner

tfranzel commented Mar 7, 2024

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
Copy link
Owner

tfranzel commented Mar 7, 2024

Might #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
Copy link
Author

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
Copy link
Owner

tfranzel commented Mar 7, 2024

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
Copy link
Contributor

ftsell commented Mar 7, 2024

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
Copy link
Author

KlodianMaloku-Rt commented Mar 11, 2024

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
Copy link
Author

@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
Copy link
Author

@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=<access_token>&token_type=Bearer&expires_in=900

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

No branches or pull requests

3 participants