jeremyschulman / netbox-plugin-auth-saml2

Netbox plugin for SSO using SAML2
119 stars 21 forks source link

Access Denied / Missing SAML response #61

Open tacerus opened 2 years ago

tacerus commented 2 years ago

Hi!

I think the issue is more related with https://github.com/jeremyschulman/django3-auth-saml2, but since I'm only using this Netbox plugin with it I hope it fits here.

I configured the plugin with Keycloak, and followed the advice and applied settings as per the screenshots from the nice fellow over in https://github.com/jeremyschulman/netbox-plugin-auth-saml2/issues/9.

The authentication itself works - Netbox login forwards to Keycloak, and after logging in there, it forwards back to Netbox - however, Netbox returns "Access Denied - You do not have permission to access this page.":

Screenshot ![image](https://user-images.githubusercontent.com/5121046/171061185-bde4c627-adce-4bba-b818-320abe9b256a.png)

After enabling debug mode in Netbox, I notice the following log output: gunicorn[30354]: django.core.exceptions.PermissionDenied: ('SAML2: missing response').

Upon studying your code, it seems this is thrown even before the individual mapped attributes are parsed, and with the comment in https://github.com/jeremyschulman/django3-auth-saml2/blob/48a5084c24556340236350bf8008b71ec5105061/django3_auth_saml2/views.py#L68, it seems to me as if the POST response data passed to the plugin is simply empty - however, I am able to find POST response data in my browser:

Screenshot ![image](https://user-images.githubusercontent.com/5121046/171061155-e100b89d-3152-4f61-aaf6-46be1ec8f799.png)

This is the SAML representation logged by Keycloak during the process:

Representation ``` { "id": "x", "clientId": "NetBox", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": true, "clientAuthenticatorType": "client-secret", "redirectUris": [ "https://netbox.intranet.squirrelcube.com/sso/acs/" ], "webOrigins": [], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": false, "publicClient": false, "frontchannelLogout": true, "protocol": "saml", "attributes": { "saml_assertion_consumer_url_redirect": "https://netbox.intranet.squirrelcube.com/sso/acs/", "saml.force.post.binding": "true", "saml.multivalued.roles": "false", "frontchannel.logout.session.required": "false", "oauth2.device.authorization.grant.enabled": "false", "backchannel.logout.revoke.offline.tokens": "false", "saml.server.signature.keyinfo.ext": "false", "use.refresh.tokens": "true", "saml.signing.certificate": "x", "oidc.ciba.grant.enabled": "false", "backchannel.logout.session.required": "false", "client_credentials.use_refresh_token": "false", "saml.signature.algorithm": "RSA_SHA256", "require.pushed.authorization.requests": "false", "saml.client.signature": "false", "saml.signing.private.key": "x", "saml.allow.ecp.flow": "false", "id.token.as.detached.signature": "false", "saml.assertion.signature": "true", "client.secret.creation.time": "1653834855", "saml.encrypt": "false", "saml_assertion_consumer_url_post": "https://netbox.intranet.squirrelcube.com/sso/acs/", "saml.server.signature": "true", "exclude.session.state.from.auth.response": "false", "saml.artifact.binding.identifier": "x", "saml.artifact.binding": "false", "saml_force_name_id_format": "true", "acr.loa.map": "{}", "tls.client.certificate.bound.access.tokens": "false", "saml.authnstatement": "true", "display.on.consent.screen": "false", "saml_name_id_format": "username", "token.response.type.bearer.lower-case": "false", "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#", "saml.onetimeuse.condition": "false" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, "protocolMappers": [ { "id": "x", "name": "username", "protocol": "saml", "protocolMapper": "saml-user-property-mapper", "consentRequired": false, "config": { "attribute.nameformat": "URI Reference", "user.attribute": "username", "attribute.name": "username" } }, { "id": "x", "name": "lastName", "protocol": "saml", "protocolMapper": "saml-user-property-mapper", "consentRequired": false, "config": { "attribute.nameformat": "URI Reference", "user.attribute": "lastName", "attribute.name": "last_name" } }, { "id": "x", "name": "groups", "protocol": "saml", "protocolMapper": "saml-group-membership-mapper", "consentRequired": false, "config": { "single": "true", "attribute.nameformat": "URI Reference", "full.path": "false", "attribute.name": "member" } }, { "id": "x", "name": "email", "protocol": "saml", "protocolMapper": "saml-user-property-mapper", "consentRequired": false, "config": { "attribute.nameformat": "URI Reference", "user.attribute": "email", "attribute.name": "email" } }, { "id": "x", "name": "firstName", "protocol": "saml", "protocolMapper": "saml-user-property-mapper", "consentRequired": false, "config": { "attribute.nameformat": "URI Reference", "user.attribute": "firstName", "attribute.name": "first_name" } } ], "defaultClientScopes": [ "role_list" ], "optionalClientScopes": [], "access": { "view": true, "configure": true, "manage": true } } ```

And this is the configuration I added in Netbox:

Configuration ``` PLUGINS_CONFIG = { 'django3_saml2_nbplugin': { 'AUTHENTICATION_BACKEND': 'django3_saml2_nbplugin.backends.SAML2AttrUserBackend', 'DEFAULT_NEXT_URL': '/', 'ASSERTION_URL': 'https://netbox.intranet.squirrelcube.com', 'ENTITY_ID': 'NetBox', 'METADATA_LOCAL_FILE_PATH': '/opt/netbox-data/saml.xml', 'ALWAYS_UPDATE_USER': True, 'FLAGS_BY_GROUP': { 'is_staff': 'syscid_netbox_staff', 'is_superuser': 'syscid_netbox_admins' } } } REMOTE_AUTH_ENABLED = True REMOTE_AUTH_BACKEND = 'django3_saml2_nbplugin.backends.SAML2CustomAttrUserBackend' REMOTE_AUTH_HEADER = 'HTTP_REMOTE_USER' REMOTE_AUTH_AUTO_CREATE_USER = True REMOTE_AUTH_DEFAULT_GROUPS = [] REMOTE_AUTH_DEFAULT_PERMISSIONS = {} ```

In an attempt to troubleshoot it, I expanded the exception to raise PermissionDenied(errmsg, dir(req), req.GET, req.POST, req.META) - it was not very helpful for me, but including it for completeness:

Long exception `gunicorn[30354]: django.core.exceptions.PermissionDenied: ('SAML2: missing response', ['COOKIES', 'FILES', 'GET', 'META', 'POST', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_cors_enabled', '_current_scheme_host', '_encoding', '_files', '_get_full_path', '_get_post', '_get_raw_host', '_get_scheme', '_initialize_handlers', '_load_post_and_files', '_mark_post_parse_error', '_messages', '_post', '_read_started', '_set_content_type_params', '_set_post', '_stream', '_upload_handlers', 'accepted_types', 'accepts', 'body', 'build_absolute_uri', 'close', 'content_params', 'content_type', 'encoding', 'environ', 'get_full_path', 'get_full_path_info', 'get_host', 'get_port', 'get_signed_cookie', 'headers', 'id', 'is_secure', 'method', 'parse_file_upload', 'path', 'path_info', 'prometheus_after_middleware_event', 'prometheus_before_middleware_event', 'read', 'readline', 'readlines', 'resolver_match', 'scheme', 'session', 'upload_handlers', 'user'], , , {'wsgi.errors': , 'wsgi.version': (1, 0), 'wsgi.multithread': True, 'wsgi.multiprocess': True, 'wsgi.run_once': False, 'wsgi.file_wrapper': , 'wsgi.input_terminated': True, 'SERVER_SOFTWARE': 'gunicorn/20.1.0', 'wsgi.input': , 'gunicorn.socket': , 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'RAW_URI': '/api/plugins/sso/acs/', 'SERVER_PROTOCOL': 'HTTP/1.0', 'HTTP_HOST': '127.0.0.1:8001', 'HTTP_CONNECTION': 'close', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.7,de-AT;q=0.3', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_DNT': '1', 'HTTP_COOKIE': 'csrftoken=x; mellon-intranet=x', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_SEC_FETCH_DEST': 'document', 'HTTP_SEC_FETCH_MODE': 'navigate', 'HTTP_SEC_FETCH_SITE': 'cross-site', 'HTTP_SEC_GPC': '1', 'HTTP_X_FORWARDED_PROTO': 'https', 'HTTP_X_FORWARDED_PORT': '443', 'HTTP_X_FORWARDED_FOR': '185.xx.xx.xx', 'HTTP_X_FORWARDED_HOST': 'netbox.intranet.squirrelcube.com', 'HTTP_X_FORWARDED_SERVER': 'netbox.intranet.squirrelcube.com', 'wsgi.url_scheme': 'https', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '49538', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8001', 'PATH_INFO': '/api/plugins/sso/acs/', 'SCRIPT_NAME': '', 'CSRF_COOKIE': 'x', 'CSRF_COOKIE_NEEDS_UPDATE': False})`

Would appreciate any ideas as to what else I could check!

Thanks a lot for any input!

Best, Georg

jclbc commented 1 year ago

hello, did you tiry with: 'AUTHENTICATION_BACKEND': REMOTE_AUTH_BACKEND, because you have 2 differents backends in your config, SAML2AttrUserBackend and SAML2CustomAttrUserBackend. regards,

tacerus commented 1 year ago

Thanks a lot for the suggestion. I will try it.

tacerus commented 1 year ago

Unfortunately, it does not seem to be valid syntax:

Mar 02 16:15:31 orpheus gunicorn[22363]:     'AUTHENTICATION_BACKEND': REMOTE_AUTH_BACKEND,
Mar 02 16:15:31 orpheus gunicorn[22363]: NameError: name 'REMOTE_AUTH_BACKEND' is not defined
jclbc commented 1 year ago

try to move the PLUGINS_CONFIG section after REMOTEAUTH variables