DefectDojo / django-DefectDojo

DevSecOps, ASPM, Vulnerability Management. All on one platform.
https://defectdojo.com
BSD 3-Clause "New" or "Revised" License
3.75k stars 1.56k forks source link

Can't add Keycloak SSO support because code param is empty #3129

Closed ansidorov closed 4 years ago

ansidorov commented 4 years ago

Bug description After adding a patch to support Keycloak SSO, if the login is successful, I receive an Authentication process error canceled.

Steps to reproduce Steps to reproduce the behavior:

  1. Add patch with support Keycloak
    
    diff --git a/dojo/context_processors.py b/dojo/context_processors.py
    index 3ae170f..d3dd192 100644
    --- a/dojo/context_processors.py
    +++ b/dojo/context_processors.py
    @@ -9,6 +9,7 @@ def globalize_oauth_vars(request):
             'GOOGLE_ENABLED': settings.GOOGLE_OAUTH_ENABLED,
             'OKTA_ENABLED': settings.OKTA_OAUTH_ENABLED,
             'GITLAB_ENABLED': settings.GITLAB_OAUTH2_ENABLED,
    +            'KEYCLOAK_ENABLED': settings.KEYCLOAK_OAUTH2_ENABLED,
             'AZUREAD_TENANT_OAUTH2_ENABLED': settings.AZUREAD_TENANT_OAUTH2_ENABLED,
             'SAML2_ENABLED': settings.SAML2_ENABLED,
             'SAML2_LOGOUT_URL': settings.SAML2_LOGOUT_URL}
    diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py
    index 13108d2..b011097 100644
    --- a/dojo/settings/settings.dist.py
    +++ b/dojo/settings/settings.dist.py
    @@ -96,6 +96,13 @@ env = environ.Env(
     DD_SOCIAL_AUTH_GITLAB_SECRET=(str, ''),
     DD_SOCIAL_AUTH_GITLAB_API_URL=(str, 'https://gitlab.com'),
     DD_SOCIAL_AUTH_GITLAB_SCOPE=(list, ['api', 'read_user', 'openid', 'profile', 'email']),
    +    DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED=(bool, False),
    +    DD_SOCIAL_AUTH_KEYCLOAK_KEY=(str, ''),
    +    DD_SOCIAL_AUTH_KEYCLOAK_SECRET=(str, ''),
    +    DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY=(str, ''),
    +    DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL=(str, ''),
    +    DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL=(str, ''),
    +    DD_SOCIAL_AUTH_KEYCLOAK_ID_KEY=(str, 'email'),
     DD_SAML2_ENABLED=(bool, False),
     DD_SAML2_METADATA_AUTO_CONF_URL=(str, ''),
     DD_SAML2_METADATA_LOCAL_FILE_PATH=(str, ''),
    @@ -320,6 +327,7 @@ AUTHENTICATION_BACKENDS = (
     'dojo.okta.OktaOAuth2',
     'social_core.backends.azuread_tenant.AzureADTenantOAuth2',
     'social_core.backends.gitlab.GitLabOAuth2',
    +    'social_core.backends.keycloak.KeycloakOAuth2',
     'django.contrib.auth.backends.RemoteUserBackend',
     'django.contrib.auth.backends.ModelBackend',
    )
    @@ -370,6 +378,14 @@ SOCIAL_AUTH_GITLAB_SECRET = env('DD_SOCIAL_AUTH_GITLAB_SECRET')
    SOCIAL_AUTH_GITLAB_API_URL = env('DD_SOCIAL_AUTH_GITLAB_API_URL')
    SOCIAL_AUTH_GITLAB_SCOPE = env('DD_SOCIAL_AUTH_GITLAB_SCOPE')

+KEYCLOAK_OAUTH2_ENABLED = env('DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED') +SOCIAL_AUTH_KEYCLOAK_KEY = env('DD_SOCIAL_AUTH_KEYCLOAK_KEY') +SOCIAL_AUTH_KEYCLOAK_SECRET = env('DD_SOCIAL_AUTH_KEYCLOAK_SECRET') +SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = env('DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY') +SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = env('DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL') +SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = env('DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL') +SOCIAL_AUTH_KEYCLOAK_ID_KEY = env('DD_SOCIAL_AUTH_KEYCLOAK_ID_KEY') + AUTH0_OAUTH2_ENABLED = env('DD_SOCIAL_AUTH_AUTH0_OAUTH2_ENABLED') SOCIAL_AUTH_AUTH0_KEY = env('DD_SOCIAL_AUTH_AUTH0_KEY') SOCIAL_AUTH_AUTH0_SECRET = env('DD_SOCIAL_AUTH_AUTH0_SECRET') @@ -422,6 +438,7 @@ LOGIN_EXEMPT_URLS = ( r'^%sfinding/image/(?P[^/]+)$' % URL_PREFIX, r'^%sapi/v2/' % URL_PREFIX, r'complete/',

{% endif %}

  • {% if KEYCLOAK_ENABLED is True %}

  • Login with Keycloak

  • {% endif %}

  •          {% if AUTH0_ENABLED is True %}
                 <div class="col-sm-offset-1 col-sm-2">
                     <button class="btn btn-success" type="button">
  • 
    2. Add environment variables for DefectDojo:
    
      DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED: 'True'
      DD_SOCIAL_AUTH_KEYCLOAK_KEY: "defectdojo-dev"
      DD_SOCIAL_AUTH_KEYCLOAK_SECRET: "3f7d1f05f-f1f4-48713-8bd0-d12z2eebbdadd"
      DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY: "<public_key>"
      DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL: "https://example.com/auth/realms/example/protocol/openid-connect/auth"
      DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL: "https://example.com/auth/realms/example/protocol/openid-connect/token"
      DD_SOCIAL_AUTH_KEYCLOAK_ID_KEY: "email"
      SOCIAL_AUTH_RAISE_EXCEPTIONS: 'True'
    
    3. Configure Keycloak as in docs: https://github.com/python-social-auth/social-core/blob/master/social_core/backends/keycloak.py#L26-L80
    4. Go to defectdojo instance and press button `Login with Keycloak`.
    5. After completed login in Keycloak redirected to page: http://defectdojo.dc:8080/complete/keycloak/?redirect_state=iMQD3TUft1WiQJJWKUWihgJZ94jlTN6m&session_state=c39bd808-ecb5-45b5-b179-166ff535f8ce
    And see error:
    ![image](https://user-images.githubusercontent.com/68654745/97680349-9f55e800-1aa7-11eb-9bab-61b619888ea0.png)
    
    6. In debug we see request params and response code
    
    args | ()
    -- | --
    kwargs | {'auth': None,  'data': {'client_id': 'defectdojo-dev',           'client_secret': '3f7d1f05f-f1f4-48713-8bd0-d12z2eebbdadd',           'code': '',           'grant_type': 'authorization_code',           'redirect_uri': 'http://defectdojo.dc:8080/complete/keycloak/?redirect_state=iMQD3TUft1WiQJJWKUWihgJZ94jlTN6m'},  'headers': {'Accept': 'application/json',              'Content-Type': 'application/x-www-form-urlencoded'},  'params': None,  'timeout': None}
    method | 'POST'
    response | <Response [400]>
    self | <social_core.backends.keycloak.KeycloakOAuth2 object at 0x7f634949a780>
    url | 'https://example.com/auth/realms/example/protocol/openid-connect/token'
    
    7. I'm create curl with this params:

    curl -X POST -d "client_id=defectdojo-dev" -d "client_secret=3f7d1f05f-f1f4-48713-8bd0-d12z2eebbdadd" -d "code=''" -d "grant_type=authorization_code" -d "redirect_uri=http://defectdojo.dc:8080/complete/keycloak/\?redirect_state\=RfDaN1Wbzs2WIdP5f5aPU3VTUb6Ijk0l" -H 'Accept: application/json' -H 'Content-Type: application/x-www-form-urlencoded' https://example.com/auth/realms/example/protocol/openid-connect/token -i HTTP/2 400 date: Fri, 30 Oct 2020 08:45:21 GMT content-type: application/json content-length: 62 set-cookie: INGRESS_SESSION_ID=1604047522.535.1076.949539; Path=/auth/realms/example/; Secure; HttpOnly cache-control: no-store pragma: no-cache strict-transport-security: max-age=15724800; includeSubDomains

    {"error":"invalid_grant","error_description":"Code not valid"}%

    Why code param is empty?
    
    8. If a go to page http://defectdojo.dc:8080/login?next=/ and press button `Login with Keycloak` again i can access to defectdojo via my user from Keycloak.
    ![image](https://user-images.githubusercontent.com/68654745/97680965-93b6f100-1aa8-11eb-869f-6638bd38b587.png)
    
    **Expected behavior**
    A clear and concise description of what you expected to happen.
    
    **Deployment method** *(select with an `X`)*
    - [X] Kubernetes
    - [X] Docker
    - [ ] setup.bash / legacy-setup.bash
    
    **Environment information**
     - Operating System: Ubuntu 18.03
     - DefectDojo Commit Message: [2020-10-27 19:03:31 +0100] 1f50e1e: Update new-release-tag-docker.yml [ (grafted, HEAD, tag: 1.9.0)]
    
    **Sample scan files** (optional)
    If applicable, add sample scan files to help reproduce your problem.
    
    **Screenshots** (optional)
    If applicable, add screenshots to help explain your problem.
    
    **Console logs** (optional)
    Log from uwsgi:
    

    uwsgi_1 | Starting new HTTPS connection (1): example.com:443 uwsgi_1 | https://example.com:443 "POST /auth/realms/example/protocol/openid-connect/token HTTP/1.1" 400 62 uwsgi_1 | Internal Server Error: /complete/keycloak/ nginx_1 | 192.168.37.2 - - [30/Oct/2020:08:59:39 +0000] "GET /complete/keycloak/?redirect_state=iMQD3TUft1WiQJJWKUWihgJZ94jlTN6m&session_state=c39bd808-ecb5-45b5-b179-166ff535f8ce HTTP/1.1" 500 143323 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36" "-" uwsgi_1 | Traceback (most recent call last): uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/utils.py", line 251, in wrapper uwsgi_1 | return func(*args, kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/oauth.py", line 401, in auth_complete uwsgi_1 | method=self.ACCESS_TOKEN_METHOD uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/oauth.py", line 373, in request_access_token uwsgi_1 | return self.get_json(*args, *kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/base.py", line 238, in get_json uwsgi_1 | return self.request(url, args, kwargs).json() uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/base.py", line 234, in request uwsgi_1 | response.raise_for_status() uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/requests/models.py", line 941, in raise_for_status uwsgi_1 | raise HTTPError(http_error_msg, response=self) uwsgi_1 | requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://example.com/auth/realms/example/protocol/openid-connect/token uwsgi_1 | uwsgi_1 | During handling of the above exception, another exception occurred: uwsgi_1 | uwsgi_1 | Traceback (most recent call last): uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/core/handlers/exception.py", line 34, in inner uwsgi_1 | response = get_response(request) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py", line 115, in _get_response uwsgi_1 | response = self.process_exception_by_middleware(e, request) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py", line 113, in _get_response uwsgi_1 | response = wrapped_callback(request, *callback_args, callback_kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func uwsgi_1 | response = view_func(request, *args, *kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view uwsgi_1 | return view_func(args, kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_django/utils.py", line 49, in wrapper uwsgi_1 | return func(request, backend, *args, kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_django/views.py", line 33, in complete uwsgi_1 | *args, *kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/actions.py", line 45, in do_complete uwsgi_1 | user = backend.complete(user=user, args, kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/base.py", line 40, in complete uwsgi_1 | return self.auth_complete(*args, kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/utils.py", line 254, in wrapper uwsgi_1 | raise AuthCanceled(args[0], response=err.response) uwsgi_1 | social_core.exceptions.AuthCanceled: Authentication process canceled uwsgi_1 | Internal Server Error: /complete/keycloak/ uwsgi_1 | Traceback (most recent call last): uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/utils.py", line 251, in wrapper uwsgi_1 | return func(*args, *kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/oauth.py", line 401, in auth_complete uwsgi_1 | method=self.ACCESS_TOKEN_METHOD uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/oauth.py", line 373, in request_access_token uwsgi_1 | return self.get_json(args, kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/base.py", line 238, in get_json uwsgi_1 | return self.request(url, *args, kwargs).json() uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/base.py", line 234, in request uwsgi_1 | response.raise_for_status() uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/requests/models.py", line 941, in raise_for_status uwsgi_1 | raise HTTPError(http_error_msg, response=self) uwsgi_1 | requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://example.com/auth/realms/example/protocol/openid-connect/token uwsgi_1 | uwsgi_1 | During handling of the above exception, another exception occurred: uwsgi_1 | uwsgi_1 | Traceback (most recent call last): uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/core/handlers/exception.py", line 34, in inner uwsgi_1 | response = get_response(request) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py", line 115, in _get_response uwsgi_1 | response = self.process_exception_by_middleware(e, request) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py", line 113, in _get_response uwsgi_1 | response = wrapped_callback(request, *callback_args, *callback_kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func uwsgi_1 | response = view_func(request, args, kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view uwsgi_1 | return view_func(*args, kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_django/utils.py", line 49, in wrapper uwsgi_1 | return func(request, backend, *args, *kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_django/views.py", line 33, in complete uwsgi_1 | args, kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/actions.py", line 45, in do_complete uwsgi_1 | user = backend.complete(user=user, *args, *kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/backends/base.py", line 40, in complete uwsgi_1 | return self.auth_complete(args, **kwargs) uwsgi_1 | File "/usr/local/lib/python3.6/site-packages/social_core/utils.py", line 254, in wrapper uwsgi_1 | raise AuthCanceled(args[0], response=err.response) uwsgi_1 | social_core.exceptions.AuthCanceled: Authentication process canceled

    
    
    **Additional context** (optional)
    Add any other context about the problem here.
    valentijnscholten commented 4 years ago

    IIRC the IdP needs to send the code in the url when redirecting back to DD. This is step5 and it's already missing there. Might be better to ask in python-social-auth as all this is handled in that library.

    dsever commented 4 years ago

    Or to speed it up to use SAML against to KEYCLOAK it work out of the box.

    ansidorov commented 4 years ago

    Or to speed it up to use SAML against to KEYCLOAK it work out of the box.

    Do you have a working example?

    I set it up this way:

          DD_SAML2_ENABLED: 'True'.
          DD_SAML2_ENTITY_ID: "defectdojo-dev"
          DD_SAML2_METADATA_AUTO_CONF_URL: "https://example.com/auth/realms/example/protocol/saml/descriptor"

    But I get an error invalid redirect_uri.

    DefectDojo logs:

    uwsgi_1         | GET to https://example.com/auth/realms/example/protocol/saml/descriptor
    uwsgi_1         | Starting new HTTPS connection (1): example.com:443
    uwsgi_1         | https://example.com:443 "GET /auth/realms/example/protocol/saml/descriptor HTTP/1.1" 200 None
    uwsgi_1         | Response status: 200
    uwsgi_1         | example.com: 'Set-Cookie: INGRESS_SESSION_ID=1604051893.223.1076.820077; HttpOnly; Path=/auth/realms/example/; Secure'
    uwsgi_1         | service(https://example.com/auth/realms/example, idpsso_descriptor, single_sign_on_service, urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
    uwsgi_1         | service => [{'__class__': 'urn:oasis:names:tc:SAML:2.0:metadata&SingleSignOnService', 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 'location': 'https://iexample.com/auth/realms/example/protocol/saml'}]
    uwsgi_1         | destination to provider: https://example.com/auth/realms/example/protocol/saml
    uwsgi_1         | REQUEST: <ns0:AuthnRequest xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion" AssertionConsumerServiceURL="/saml2/acs/" Destination="https://example.com/auth/realms/example/protocol/saml" ID="id-IRJ0z0yH6JZM0NiZp" IssueInstant="2020-10-30T09:58:12Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">defectdojo-dev</ns1:Issuer></ns0:AuthnRequest>
    uwsgi_1         | AuthNReq: <ns0:AuthnRequest xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion" AssertionConsumerServiceURL="/saml2/acs/" Destination="https://example.com/auth/realms/example/protocol/saml" ID="id-IRJ0z0yH6JZM0NiZp" IssueInstant="2020-10-30T09:58:12Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">defectdojo-dev</ns1:Issuer></ns0:AuthnRequest>
    uwsgi_1         | HTTP REDIRECT
    uwsgi_1         | [pid: 22|app: 0|req: 4/4] 192.168.37.2 () {44 vars in 916 bytes} [Fri Oct 30 09:58:11 2020] GET /saml2/login/ => generated 0 bytes in 542 msecs (HTTP/1.1 302) 8 headers in 854 bytes (1 switches on core 1)
    nginx_1         | 192.168.37.2 - - [30/Oct/2020:09:58:12 +0000] "GET /saml2/login/ HTTP/1.1" 302 0 "http://defectdojo.dc:8080/login?next=/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36" "-"

    Client in Keycloak: image

    dsever commented 4 years ago
     DD_SAML2_ENABLED: 'true'
      DD_SAML2_METADATA_AUTO_CONF_URL: 'http://idp_FQDN/auth/realms/master/protocol/saml/descriptor'
      DD_SAML2_ENTITY_ID: 'https://defect-dojourl/saml2_auth/acs/' # need to be mapped to IDP
      DD_SAML2_ASSERTION_URL: 'https://DD_FQDN'
      DD_SAML2_ATTRIBUTE_MAPS_EMAIL: 'email'
      DD_SAML2_ATTRIBUTE_MAPS_USERNAME: 'User.Username'
      DD_SAML2_ATTRIBUTE_MAPS_FIRSTNAME: 'first_name'
      DD_SAML2_ATTRIBUTE_MAPS_LASTNAME: 'last_name'

    If you don't know what to map use SAMLTracer

    image

    AFAIK SLO is implemented as well, but I have to test it first.

    I hope this helps

    ansidorov commented 4 years ago

    @dsever Thank you very much, everything worked out perfectly.