omab / python-social-auth

Social auth made simple
http://psa.matiasaguirre.net
BSD 3-Clause "New" or "Revised" License
2.83k stars 1.09k forks source link

Fix nonce errors in OpenID Connect authentication #995

Closed aleksihakli closed 7 years ago

aleksihakli commented 8 years ago

Nonce values are committed multiple times to the database in different phases of authentication.

The specification requires one nonce value to protect the session from from replay attacks.

The current implementation claims nonce values are used for CSRF protection, which is false.

This commit fixes the use of multiple tokens by using one token to protect from replay and fixes an issue where authentication fails with different authorization and token endpoints.


As far as I understand the python-social-auth OpenID Connect implementation is somewhat broken at this moment.

The current implementation fails when the user has differing endpoints defined for authorization requests and token requests. A nonce value is first created in the authorization phase, then later in the token request phase. The token should only be created once per the session. The received ID Token should be decrypted and then validated against the nonce value that is created in the authorization phase, not in the later phase.

The following flow is used when calling the OpenIdConnectAuth class authentication:


The fix:


This is the example that brought forth the problems in the current implementation. I have a Keycloak server that I'm integrating as a SSO source:

# requirements.txt

# OAuth and OpenID auth applications
python-social-auth==0.2.21
PyJWT==1.4.2
python3-openid==3.0.10
requests-oauthlib==0.6.2
cryptography==1.4
# settings.py

AUTHENTICATION_BACKENDS = [
    'project.backends.KeycloakOpenIDConnect',
    'django.contrib.auth.backends.ModelBackend',
]

SOCIAL_AUTH_KEYCLOAK_KEY = env('DJANGO_SSO_CLIENT_ID')
SOCIAL_AUTH_KEYCLOAK_SECRET = env('DJANGO_SSO_CLIENT_SECRET')
SOCIAL_AUTH_KEYCLOAK_ID_TOKEN_JWT_DECODE_KWARGS = {
    'key': env('DJANGO_SSO_PUBLIC_KEY'),
    'algorithms': [
        'RS256',
        'HS256'
    ],
}

SOCIAL_AUTH_KEYCLOAK_REALM = env('DJANGO_SSO_REALM')
SOCIAL_AUTH_KEYCLOAK_URL_ISSUER = 'https://keycloak.example.com/auth/realms/{0}'.format(
    SOCIAL_AUTH_KEYCLOAK_REALM
)
SOCIAL_AUTH_KEYCLOAK_URL_ROOT = '{0}/protocol/openid-connect'.format(
    SOCIAL_AUTH_KEYCLOAK_URL_ISSUER
)
# backends.py

from django.conf import settings

from social.backends.open_id import OpenIdConnectAuth

class KeycloakOpenIDConnect(OpenIdConnectAuth):
    name = 'keycloak'
    ID_TOKEN_ISSUER = settings.SOCIAL_AUTH_KEYCLOAK_URL_ISSUER
    AUTHORIZATION_URL = '{0}/auth'.format(
        settings.SOCIAL_AUTH_KEYCLOAK_URL_ROOT
    )
    ACCESS_TOKEN_URL = '{0}/token'.format(
        settings.SOCIAL_AUTH_KEYCLOAK_URL_ROOT
    )
    ACCESS_TOKEN_METHOD = 'POST'

    def get_user_details(self, response):
        # Required for correct operation; represented as a stub here
        return {}

As you can see, there are two different URLs for authorization and access tokens. When we create nonce requests with both and only fetch the valid requests with the settings.ACCESS_TOKEN_URL key, we are dismissing the firstly created valid nonce which should indeed be used. We are also not ever using the AUTHORIZATION_URL based nonce, which should in fact be used according the specification. It is merely created, and it is certainly not used for CSRF protection.


Sources: