GithubHelp home page GithubHelp logo

drf-oidc-auth's Introduction

OpenID Connect authentication for Django Rest Framework

This package contains an authentication mechanism for authenticating users of a REST API using tokens obtained from OpenID Connect.

Currently, it only supports JWT and Bearer tokens. JWT tokens will be validated against the public keys of an OpenID connect authorization service. Bearer tokens are used to retrieve the OpenID UserInfo for a user to identify him.

Installation

Install using pip:

pip install drf-oidc-auth

Configure authentication for Django REST Framework in settings.py:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        # ...
        'oidc_auth.authentication.JSONWebTokenAuthentication',
        'oidc_auth.authentication.BearerTokenAuthentication',
    ),
}

And configure the module itself in settings.py:

OIDC_AUTH = {
    # Specify OpenID Connect endpoint. Configuration will be
    # automatically done based on the discovery document found
    # at <endpoint>/.well-known/openid-configuration
    'OIDC_ENDPOINT': 'https://accounts.google.com',

    # The Claims Options can now be defined by a static string.
    # ref: https://docs.authlib.org/en/latest/jose/jwt.html#jwt-payload-claims-validation
    # The old OIDC_AUDIENCES option is removed in favor of this new option.
    # `aud` is only required, when you set it as an essential claim.
    'OIDC_CLAIMS_OPTIONS': {
        'aud': {
            'values': ['myapp'],
            'essential': True,
        }
    },
    
    # (Optional) Function that resolves id_token into user.
    # This function receives a request and an id_token dict and expects to
    # return a User object. The default implementation tries to find the user
    # based on username (natural key) taken from the 'sub'-claim of the
    # id_token.
    'OIDC_RESOLVE_USER_FUNCTION': 'oidc_auth.authentication.get_user_by_id',
    
    # (Optional) Number of seconds in the past valid tokens can be 
    # issued (default 600)
    'OIDC_LEEWAY': 600,
    
    # (Optional) Time before signing keys will be refreshed (default 24 hrs)
    'OIDC_JWKS_EXPIRATION_TIME': 24*60*60,

    # (Optional) Time before bearer token validity is verified again (default 10 minutes)
    'OIDC_BEARER_TOKEN_EXPIRATION_TIME': 10*60,
    
    # (Optional) Token prefix in JWT authorization header (default 'JWT')
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
    
    # (Optional) Token prefix in Bearer authorization header (default 'Bearer')
    'BEARER_AUTH_HEADER_PREFIX': 'Bearer',

    # (Optional) Which Django cache to use
    'OIDC_CACHE_NAME': 'default',

    # (Optional) A cache key prefix when storing and retrieving cached values
    'OIDC_CACHE_PREFIX': 'oidc_auth.',
}

Running tests

pip install tox
tox

Mocking authentication

There's a AuthenticationTestCaseMixin provided in the oidc_auth.test module, which you can use for testing authentication like so:

from oidc_auth.test import AuthenticationTestCaseMixin
from django.test import TestCase

class MyTestCase(AuthenticationTestCaseMixin, TestCase):
    def test_example_cache_of_valid_bearer_token(self):
        self.responder.set_response(
            'http://example.com/userinfo', {'sub': self.user.username})
        auth = 'Bearer egergerg'
        resp = self.client.get('/test/', HTTP_AUTHORIZATION=auth)
        self.assertEqual(resp.status_code, 200)

        # Token expires, but validity is cached
        self.responder.set_response('http://example.com/userinfo', "", 401)
        resp = self.client.get('/test/', HTTP_AUTHORIZATION=auth)
        self.assertEqual(resp.status_code, 200)

    def test_example_using_invalid_bearer_token(self):
        self.responder.set_response('http://example.com/userinfo', "", 401)
        auth = 'Bearer hjikasdf'
        resp = self.client.get('/test/', HTTP_AUTHORIZATION=auth)
        self.assertEqual(resp.status_code, 401)

References

drf-oidc-auth's People

Contributors

alejandrogarza avatar alexandergrooff avatar alexdutton avatar allardhoeve avatar cellebyte avatar dedsm avatar dependabot[bot] avatar juyrjola avatar kevin-brown avatar maartenkos avatar mvschaik avatar suutari-ai avatar thorekr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

drf-oidc-auth's Issues

.well-known/openid-configuration called at each request

hello,

I noticed that the endpoint OIDC_ENDPOINT/.well-known/openid-configuration is called on each request in the oidc_config cached_property of BaseOidcAuthentication.
This adds consequent latency on each call.
It is used to get the issuer via

    @cached_property
    def issuer(self):
        return self.oidc_config['issuer']

when validating the jwt at

        if id_token.get('iss') != self.issuer:
            msg = _('Invalid Authorization header. Invalid JWT issuer.')
            raise AuthenticationFailed(msg)

Could we replace the line above

        if id_token.get('iss') != self.issuer:

by

        if id_token.get('iss') != api_settings.OIDC_ENDPOINT:

or would there be unintended consequences of this change ?

First Look: Understanding DRF OpenID Connect Auth

I have been looking a few different projects for OpenID implementation until I stumbled across this one, and decided I should check out the source since there isn't a whole lot of documentation to know if it will suit my needs. After reading, a few questions came to mind:

  1. Where do I specify a client secret to secure the connection?
  2. Where are the actual API views and urls for a login and callback?
  3. How do I best redirect from these views to my frontend upon logging in?

JWEST not maintened anymore

Hello !

Look like https://github.com/IdentityPython/pyjwkest is not maintened since 12/05/2020.

Today our docker image crash with the error:

ImportError: Could not import 'oidc_auth.authentication.JSONWebTokenAuthentication' for API setting 'DEFAULT_AUTHENTICATION_CLASSES'. ModuleNotFoundError: No module named 'jwkest'.

Django 4.0 Support - ugettext removed from django.utils.translation import

It looks like version 2.0.0 of this library isn't compatible with Django 4.0. I just upgraded from 3.0.4 to 4.0.4 and I go the below stack when trying to start the server:

File "/Users/.../Library/Python/3.8/lib/python/site-packages/oidc_auth/authentication.py", line 13, in <module>
    from django.utils.translation import ugettext as _

I'm not all sure what this import supports or how to upgrade it, but it's kind of a dealbreaker for anyone running Django 4.0

from django.utils.translation import ugettext as _

OIDC_EXTRA_SCOPE_CLAIMS in OIDC_AUTH

Hello!

Any way of using OIDC_EXTRA_SCOPE_CLAIMS in OIDC_AUTH or this is something that's available only in django-oidc-provider? I'm trying to add extra claims to the token (i.e. user's department) but I'm not sure if it's possible do it only by using this package.

Thank you.

Re-adding the same key breaks proper expiration

Hi !
I have this error on our server.
I suspect that two requests arrive at the same time are creating a race congition.
We use
Gunicorn 20.0.4
Django==2.2.11
djangorestframework==3.11.0
drf-oidc-auth==0.9

Can you help ?

Traceback (most recent call last):
File "/usr/local/lib/python3.6/site-packages/oidc_auth/util.py", line 35, in wrapped
cached_value = self.get_from_cache(args)
File "/usr/local/lib/python3.6/site-packages/oidc_auth/util.py", line 27, in get_from_cache
return self.cached_values[key]
KeyError: (b'ey...fQ.ey...n0.Dl...wQ',)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/local/lib/python3.6/site-packages/django/core/handlers/exception.py", line 34, in inner
response = get_response(request)
File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py", line 115, in _get_response
response = self.process_exception_by_middleware(e, request)
File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py", line 113, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.6/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/usr/local/lib/python3.6/site-packages/django/views/generic/base.py", line 71, in view
return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py", line 505, in dispatch
response = self.handle_exception(exc)
File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py", line 465, in handle_exception
self.raise_uncaught_exception(exc)
File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py", line 476, in raise_uncaught_exception
raise exc
File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py", line 493, in dispatch
self.initial(request, *args, **kwargs)
File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py", line 410, in initial
self.perform_authentication(request)
File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py", line 324, in perform_authentication
request.user
File "/usr/local/lib/python3.6/site-packages/rest_framework/request.py", line 220, in user
self._authenticate()
File "/usr/local/lib/python3.6/site-packages/rest_framework/request.py", line 373, in _authenticate
user_auth_tuple = authenticator.authenticate(self)
File "/usr/local/lib/python3.6/site-packages/oidc_auth/authentication.py", line 44, in authenticate
userinfo = self.get_userinfo(bearer_token)
File "/usr/local/lib/python3.6/site-packages/oidc_auth/util.py", line 38, in wrapped
self.add_to_cache(args, cached_value, now)
File "/usr/local/lib/python3.6/site-packages/oidc_auth/util.py", line 20, in add_to_cache
assert key not in self.cached_values, "Re-adding the same key breaks proper expiration"
AssertionError: Re-adding the same key breaks proper expiration

OIDC_LEEWAY is used even if there is an exp property on the JWT tokens

Hi,
even if there is an exp property on the token, the code verify the iat property (issued at) + OIDC_LEEWAY. Even if no OIDC_LEEWAY configured by the usr since there is a default.

This is lead to a lot of confusion in our team. Should this really be the default behavior? And if yes maybe it can be better documented?

Inconsistent handling of leeway?

Hi,

First of all, thank you for creating + maintaining this package.

I'm on drf-oidc-auth version 1.0.0 and Authlib 0.15.3.

I was looking into the code for handling the 'exp' and 'iat' headers, and I have the feeling there's some inconsistency in how the leeway parameter is used/interpreted. Let's introduce two names:
relative leeway: a time delta. Accept the header if the difference between current time and stated time is no more than this time delta.
absolute leeway: a boundary value obtained from subtracting the relative leeway from the current time.

In class JSONWebTokenAuthentication, method validate_claims in oidc_auth/authentication.py, the function id_token.validate is called with an absolute leeway:

            id_token.validate(
                now=int(time.time()),
                leeway=int(time.time()-api_settings.OIDC_LEEWAY)
            )

The DRFIDToken defined in oidc_auth/authentication.py indeed seems to assume an absolute leeway (self['iat'] < leeway):

class DRFIDToken(IDToken):

    def validate_exp(self, now, leeway):
        super(DRFIDToken, self).validate_exp(now, leeway)
        if now > self['exp']:
            msg = _('Invalid Authorization header. JWT has expired.')
            raise AuthenticationFailed(msg)

    def validate_iat(self, now, leeway):
        super(DRFIDToken, self).validate_iat(now, leeway)
        if self['iat'] < leeway:
            msg = _('Invalid Authorization header. JWT too old.')
            raise AuthenticationFailed(msg)

However, the validation of exp in package authlib , file rfc7519/claims.py (which is what the super call leads to) looks as follows:

    def validate_exp(self, now, leeway):
        """The "exp" (expiration time) claim identifies the expiration time on
        or after which the JWT MUST NOT be accepted for processing.  The
        processing of the "exp" claim requires that the current date/time
        MUST be before the expiration date/time listed in the "exp" claim.
        Implementers MAY provide for some small leeway, usually no more than
        a few minutes, to account for clock skew.  Its value MUST be a number
        containing a NumericDate value.  Use of this claim is OPTIONAL.
        """
        if 'exp' in self:
            exp = self['exp']
            if not _validate_numeric_time(exp):
                raise InvalidClaimError('exp')
            if exp < (now - leeway):
                raise ExpiredTokenError()

The line exp < (now - leeway) is a check for a relative leeway.

I notice that there has been a recent change from jwkest to authlib, so I guess this may be related. However, I haven't been able to find exact proof that this is where this inconsistency originates.

Could you confirm if my analysis is correct and/or sounds plausible? And if so, what your ideal solution would look like? I could imagine that the checks in this package's code would also have to change to relative leeways. If so, I might be able to supply a pull request fixing this.

Thanks,
Florian

Support POST for validating Bearer token against userinfo endpoint

Some providers like django-oidc are supporting receiving the user data also via POST against userinfo endpoint. We should support a option to do the request via POST instead of GET.
This can help to prevent default issues with caching on the endpoint.

Best
ZuSe

Async version

HI there,

I haven't been using this lib for at least one year on various project. I'm now working on a project where we're using channels in async mode to handle websockets. We still use an oidc server and this lib to handle the requests, however, we can't execute it in async mode, because of databases calls that are considered as unsafe. That's not a huge problem as we're simply going to rewrite it to make it async using aiohttp instead of request and database_sync_to_async to encapsulate database calls.

As we're doing it anyway, I was wondering if you'd like us to share the code ?

Thanks for your work !

simplejson.scanner.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Traceback (most recent call last):
  File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/base.py", line 249, in _legacy_get_response
    response = self._get_response(request)
  File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.5/dist-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/rest_framework/viewsets.py", line 103, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/rest_framework/views.py", line 483, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python3.5/dist-packages/rest_framework/views.py", line 443, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/usr/local/lib/python3.5/dist-packages/rest_framework/views.py", line 471, in dispatch
    self.initial(request, *args, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/rest_framework/views.py", line 388, in initial
    self.perform_authentication(request)
  File "/usr/local/lib/python3.5/dist-packages/rest_framework/views.py", line 314, in perform_authentication
    request.user
  File "/usr/local/lib/python3.5/dist-packages/rest_framework/request.py", line 222, in user
    self._authenticate()
  File "/usr/local/lib/python3.5/dist-packages/rest_framework/request.py", line 375, in _authenticate
    user_auth_tuple = authenticator.authenticate(self)
  File "/usr/local/lib/python3.5/dist-packages/oidc_auth/authentication.py", line 47, in authenticate
    userinfo = self.get_userinfo(bearer_token)
  File "/usr/local/lib/python3.5/dist-packages/oidc_auth/util.py", line 40, in wrapped
    cached_value = fn(this, *args)
  File "/usr/local/lib/python3.5/dist-packages/oidc_auth/authentication.py", line 74, in get_userinfo
    response = requests.get(self.oidc_config['userinfo_endpoint'],
  File "/usr/local/lib/python3.5/dist-packages/django/utils/functional.py", line 35, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/usr/local/lib/python3.5/dist-packages/oidc_auth/authentication.py", line 35, in oidc_config
    return requests.get(api_settings.OIDC_ENDPOINT + '/.well-known/openid-configuration', verify=default_ssl_check).json()
  File "/usr/local/lib/python3.5/dist-packages/requests/models.py", line 892, in json
    return complexjson.loads(self.text, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/simplejson/__init__.py", line 516, in loads
    return _default_decoder.decode(s)
  File "/usr/local/lib/python3.5/dist-packages/simplejson/decoder.py", line 370, in decode
    obj, end = self.raw_decode(s)
  File "/usr/local/lib/python3.5/dist-packages/simplejson/decoder.py", line 400, in raw_decode
    return self.scan_once(s, idx=_w(s, idx).end())
simplejson.scanner.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

When I add Authorization Bearer and I have this error

Deprecation Warning in Django 3.0

Hi Team,

Thanks for the project, it works really well when working with Auth0!

I recently upgraded to django 3.0 which is deprecating django.utils.encoding.smart_text() in favor of django.utils.encoding.smart_str()

I think think smart_str() works as a drop-in replacement for the smart_text() call in line 59 of authentication.py. The only difference between the two is that smart_str() keeps lazy objects as lazy objects, and smart_text() resolves lazy objects to strings.

Happy to submit a PR if you agree that this would not have any unexpected consequences.

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.