Giter Site home page Giter Site logo

mozilla-django-oidc's Introduction

mozilla-django-oidc

https://circleci.com/gh/mozilla/mozilla-django-oidc/tree/main.svg?style=svg

A lightweight authentication and access management library for integration with OpenID Connect enabled authentication services.

Documentation

The full documentation is at https://mozilla-django-oidc.readthedocs.io.

Design principles

  • Keep it as minimal/lightweight as possible
  • Store as few authn/authz artifacts as possible
  • Allow custom functionality by overriding the authentication backend
  • Mainly support OIDC authorization code flow
  • Allow shipping Mozilla-centric authn/authz features
  • Test against all supported Python/Django version
  • E2E tested and audited by Mozilla InfoSec

Running Unit Tests

Use tox to run as many different versions of Python you have. If you don't have tox installed (and executable) already you can either install it in your system Python or https://pypi.python.org/pypi/pipsi. Once installed, simply execute in the project root directory.

$ tox

tox will do the equivalent of installing virtual environments for every combination mentioned in the tox.ini file. If your system, for example, doesn't have python3.4 those tox tests will be skipped.

For a faster test-rinse-repeat cycle you can run tests in a specific environment with a specific version of Python and specific version of Django of your choice. Here is such an example:

$ virtualenv -p /path/to/bin/python3.8 venv
$ source venv
(venv) $ pip install -r requirements/requirements_dev.txt
(venv) $ DJANGO_SETTINGS_MODULE=tests.settings django-admin test

Measuring code coverage, continuing the steps above:

(venv) $ pip install coverage
(venv) $ DJANGO_SETTINGS_MODULE=tests.settings coverage run --source mozilla_django_oidc `which django-admin` test
(venv) $ coverage report
(venv) $ coverage html
(venv) $ open htmlcov/index.html

Local development

The local development setup is based on Docker so you need the following installed in your system:

  • docker
  • docker-compose

You will also need to edit your hosts file to resolve testrp and testprovider hostnames to 127.0.0.1.

Running test services

To run the testrp and testprovider instances run the following:

(venv) $ docker-compose up -d testprovider testrp

Then visit the testing django app on: http://testrp:8081.

The library source code is mounted as a docker volume and source code changes are reflected directly in. In order to test a change you need to restart the testrp service.

(venv) $ docker-compose stop testrp
(venv) $ docker-compose up -d testrp

Running integration tests

Integration tests are mounted as a volume to the docker containers. Tests can be run using the following command:

(venv) $ docker-compose run --service-ports testrunner

Linting

All code is checked with https://pypi.python.org/pypi/flake8 in continuous integration. To make sure your code still passes all style guides install flake8 and check:

$ flake8 mozilla_django_oidc tests

Note

When you run tox it also does a flake8 run on the main package files and the tests.

You can also run linting with tox:

$ tox -e lint

Finally you can use pre-commit hooks to run linting and formatting before you commit your code:

(venv)  $ pre-commit install

Releasing a new version

mozilla-django-oidc releases are hosted in PyPI. Here are the steps you need to follow in order to push a new release:

  • Make sure that HISTORY.rst is up-to-date focusing mostly on backwards incompatible changes.

    Security vulnerabilities should be clearly marked in a "Security issues" section along with a level indicator of:

    • High: vulnerability facilitates data loss, data access, impersonation of admin, or allows access to other sites or components

      Users should upgrade immediately.

    • Medium: vulnerability endangers users by sending them to malicious sites or stealing browser data.

      Users should upgrade immediately.

    • Low: vulnerability is a nuissance to site staff and/or users

      Users should upgrade.

  • Bump the project version and create a commit for the new version.

    • You can use bumpversion for that. It is a tool to automate this procedure following the semantic versioning scheme.
      • For a patch version update (eg 0.1.1 to 0.1.2) you can run bumpversion patch.
      • For a minor version update (eg 0.1.0 to 0.2.0) you can run bumpversion minor.
      • For a major version update (eg 0.1.0 to 1.0.0) you can run bumpversion major.
  • Create a signed tag for that version

    Example:

    git tag -s 0.1.1 -m "Bump version: 0.1.0 to 0.1.1"
    
  • Push the signed tag to Github

    Example:

    git push origin 0.1.1
    

The release is pushed automatically to PyPI using a travis deployment hook on every new tag.

License

This software is licensed under the MPL 2.0 license. For more info check the LICENSE file.

Credits

Tools used in rendering this package:

mozilla-django-oidc's People

Contributors

akatsoulas avatar anlutro avatar cfra avatar davidjb avatar djmitche avatar eduardrosert avatar escattone avatar germanoguerrini avatar jaap3 avatar jannh avatar jezdez avatar johngian avatar johnpaulett avatar justinazoff avatar jwhitlock avatar melanger avatar mklan avatar mozilla-github-standards avatar olleolleolle avatar peterbe avatar ppapadeas avatar puiterwijk avatar robhudson avatar tpazderka avatar traylenator avatar vanschelven avatar viggiem avatar willkg avatar woutor avatar yoctozepto 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  avatar  avatar  avatar  avatar  avatar  avatar

mozilla-django-oidc's Issues

Don't call `user.is_authenticated` in Django >= 1.10

As of Django 1.10 request.user.is_authenticated is no longer a method. It's a property. So doing request.user.is_authenticated() is like doing True().

Relevant line.

Note: #123 is in the midst of changing the relevant line so let's hold off with a patch just a smidge to avoid annoying conflicts.

Check state string on OIDC callback

We should store the random state string generated in OIDC authentication request in user session and verify that the callback has the same string in it to mitigate CSRF, XSS and other attack vectors.

"import_from_string" fails when default=False

The way we check for default_val in import_string is wrong. If default_val==False or default_val=='' and the config entry trying to get is not defined, it raises an ImproperlyConfigured error.

Base64 encoding raises error when using Python3

When OIDC_RP_CLIENT_SECRET_ENCODED==False authentication raises the following error on callback:

Exception Type: TypeError at /oidc/callback/
Exception Value: a bytes-like object is required, not 'str'

This gets triggered on python 3.x only.

No test coverage of OIDC_OP_LOGOUT_URL_METHOD

When working on #105 I noticed that we're missing some test coverage on views.py:

screen shot 2017-05-22 at 8 46 33 am

I'm not actually familiar with that line but it looks rather complex so we should probably have test coverage of it.

create an oidc mock

It would make it much easier if we had a mock for this library that let developers test authentication flows so they can write tests for their site that cover "what happens if the user doesn't exist?", "what happens if the user exists, but is disabled?", "what happens if the user claim data looks like this?", etc. Testing authentication flows is a pain in the ass. A mock would make that a lot easier even if it only covered a few scenarios.

This could be solved in a few different ways:

  1. write our own mock that comes with the library code and document using it
  2. write a shim that lets people use some other library and document using it
  3. find another library that works well and document using it

Handle user creation.

Based on the settings provided by the user, we need to handle user creation in the authenticate method.
Django-browserid has a nice implementation.

add Jinja2 examples

A lot of Mozilla sites are using Jinja2 and django-jinja, so it's prudent to have Jinja2 examples to make everyone's life easier.

The specified alg value is not allowed

JWSError at /oidc/callback/
The specified alg value is not allowed
Request Method:	GET
Request URL:	http://localhost:8000/oidc/callback/?state=hCEEE41jE8IWbb0tPmOF2pQDNN2mDtID&code=4/YNB06BKYHJqIYVCExuns0yRLMxJ58oZyGTys-dhySuk&authuser=0&session_state=e94e5eed1e3d50a875039d17038a181ee7343e0c..0010&prompt=consent
Django Version:	1.10.4
Exception Type:	JWSError
Exception Value:	
The specified alg value is not allowed
Exception Location:	/Users/ranvijay.s/oidc-rnd/env/lib/python2.7/site-packages/jose/jws.py in _verify_signature, line 258
Python Executable:	/Users/ranvijay.s/oidc-rnd/env/bin/python

document how auth0 identities match to django identities

There's no documentation covering how mozilla-django-oidc takes an auth0 identity from a user_token and figures out which Django user that belongs to.

It's pretty important for any site that has to migrate from an existing system to this one. Further, surfacing how that works will likely surface inherent restrictions regarding accounts.

match OIDC identity to Django user issues

Right now, mozilla-django-oidc matches an OIDC identity to a Django user via the Django user email field.

That's problematic for a couple of reasons:

  1. the code is doing a case-sensitive exact match, so it's possible that could fail if the case ever changes (email addresses on some systems are case-insensitive)
  2. the email field in the Django user table is not marked as unique and thus you can have multiple Django users with the same email field

I think to fix item 1, we want to do a case-insensitive match even though technically, email addresses are case-sensitive.

I think to fix item 2, we want to be using the username field of the Django user table and not the email field. Django 1.11's username field works fine with that. It means we don't have to generate usernames from email addresses. It means two users can't have the same email address. It solves a bunch of things for us.

Are there other issues we need to work around?

Are there better ways to solve this?

tox everything

The README suggests running tests with python.
Apparently the travis file uses its own matrix.
Ideally everything should be all tox.

Also, the documentation should guide developers how to run individual tests rapidly without having to run the whole suite across all versions.

I'm happy to work on this.

document OIDC_CREATE_USER

When working on PR #95, one of the things that came up was whether we should also document OIDC_CREATE_USER in that PR.

From the comments:

@willkg: If you have it set to False, how does the flow work? Don't you end up with a user that's authenticated, but has no corresponding Django user? What do you do at that point?

@akatsoulas: That's a good point. A possible scenario could be the one where the authenticate method can be overridden for the use case where a developer needs just to verify an email/user data like eg in a campaign and save them (completely ignoring the Django user). That said, probably this is a hack that does not conform with the Django flow and needs some more work/thought. An idea could be to handle cases like the above at an earlier stage of the flow. @willkg @johngian thoughts?

@willkg: I like that there's an "escape hatch", but I think any documentation for the setting should probably include some guidance on why you'd want to use it and how to use it.

This issue covers documenting that setting along with some explanation for why you might want to do that and what you need to implement to make it work well.

Handle next URL parameter to redirect after login

It's common when handling logins to django to pass an extra next URL param to define where user should be redirected after successful login. In our case

  • On authentication initialization we should optionally get request.GET["next"] and store it in user session (if it exists)
  • On callback we should pop this from the session and redirect user after successful login.

if LOGOUT_REDIRECT_URL is not set, it defaults to None

If LOGOUT_REDIRECT_URL is not set, then it defaults to None in Django 1.10+.

The settings docs suggest it defaults to /, but it doesn't. So if you don't set it, then after going through the OIDCLogoutView dispatch code, the user gets redirected to None which ends up being /oidc/logout/None which is a 404.

This issue covers figuring this out.

User username field shouldn't get a bytes value

The Django username field should get a unicode value in Python 2 and a string value in Python 3.

However, it's getting the output of default_username_algo which generates a string in Python 2 (which is fine since there are no non-ascii characters in it) and a bytes in Python 3 (which is probably not fine).

Further, the signature for username generation functions is "weird" in the sense that it takes a string/bytes as an argument and returns a string/bytes--but it operates on an email address and returns a username. That feels weird.

This issue covers fixing this part of the API.

UnboundLocalError on logout

See traceback below.
Happens when I try to go to http://localhost:8000/oidc/logout/

The relevant code assumes that the user is authenticated.

web_1          | Traceback (most recent call last):
web_1          |   File "/usr/local/lib/python3.5/wsgiref/handlers.py", line 137, in run
web_1          |     self.result = application(self.environ, self.start_response)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/contrib/staticfiles/handlers.py", line 63, in __call__
web_1          |     return self.application(environ, start_response)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/wsgi.py", line 157, in __call__
web_1          |     response = self.get_response(request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py", line 124, in get_response
web_1          |     response = self._middleware_chain(request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/exception.py", line 43, in inner
web_1          |     response = response_for_exception(request, exc)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/exception.py", line 93, in response_for_exception
web_1          |     response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/exception.py", line 41, in inner
web_1          |     response = get_response(request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py", line 249, in _legacy_get_response
web_1          |     response = self._get_response(request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response
web_1          |     response = self.process_exception_by_middleware(e, request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response
web_1          |     response = wrapped_callback(request, *callback_args, **callback_kwargs)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/views/generic/base.py", line 68, in view
web_1          |     return self.dispatch(request, *args, **kwargs)
web_1          |   File "/usr/local/lib/python3.5/site-packages/mozilla_django_oidc/views.py", line 128, in dispatch
web_1          |     return HttpResponseRedirect(logout_url)
web_1          | UnboundLocalError: local variable 'logout_url' referenced before assignment

Consider a name change?

Having the word "mozilla" in the name implies (at least to me) that this is extremely Mozilla specific. But I think that's not true; it's Django and an OAuth2 provider, right? Nothing specific to Mozilla.

At first I thought, why not just call it "django-oidc" but then I noticed this: #106

jws.verify() returns a byte string

I'm currently getting this error:

web_1          | Traceback (most recent call last):
web_1          |   File "/usr/local/lib/python3.5/wsgiref/handlers.py", line 137, in run
web_1          |     self.result = application(self.environ, self.start_response)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/contrib/staticfiles/handlers.py", line 63, in __call__
web_1          |     return self.application(environ, start_response)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/wsgi.py", line 157, in __call__
web_1          |     response = self.get_response(request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py", line 124, in get_response
web_1          |     response = self._middleware_chain(request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/exception.py", line 43, in inner
web_1          |     response = response_for_exception(request, exc)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/exception.py", line 93, in response_for_exception
web_1          |     response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/exception.py", line 41, in inner
web_1          |     response = get_response(request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py", line 249, in _legacy_get_response
web_1          |     response = self._get_response(request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response
web_1          |     response = self.process_exception_by_middleware(e, request)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response
web_1          |     response = wrapped_callback(request, *callback_args, **callback_kwargs)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/views/generic/base.py", line 68, in view
web_1          |     return self.dispatch(request, *args, **kwargs)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/views/generic/base.py", line 88, in dispatch
web_1          |     return handler(request, *args, **kwargs)
web_1          |   File "/usr/local/lib/python3.5/site-packages/mozilla_django_oidc/views.py", line 59, in get
web_1          |     self.user = auth.authenticate(**kwargs)
web_1          |   File "/usr/local/lib/python3.5/site-packages/django/contrib/auth/__init__.py", line 100, in authenticate
web_1          |     user = backend.authenticate(*args, **credentials)
web_1          |   File "/usr/local/lib/python3.5/site-packages/mozilla_django_oidc/auth.py", line 121, in authenticate
web_1          |     if self.verify_token(id_token, nonce=nonce):
web_1          |   File "/usr/local/lib/python3.5/site-packages/mozilla_django_oidc/auth.py", line 82, in verify_token
web_1          |     token_nonce = json.loads(verified_token).get('nonce')
web_1          |   File "/usr/local/lib/python3.5/json/__init__.py", line 312, in loads
web_1          |     s.__class__.__name__))
web_1          | TypeError: the JSON object must be str, not 'bytes'

It happens because of these lines:

verified_token = jws.verify(token, secret, algorithms=['HS256'])
token_nonce = json.loads(verified_token).get('nonce')

The verified_token variable becomes a byte string (Python 3.5 in my docker). E.g. b'{"iss":"https://auth.mozilla.auth0.com/","sub":"a...

Can you can't send in a byte string to json.loads().

Fix RP related settings variable names

The following settings:

OIDC_OP_CLIENT_SECRET
OIDC_OP_CLIENT_ID

should be changed to:

OIDC_RP_CLIENT_SECRET
OIDC_RP_CLIENT_ID

Also VERIFY_SSL looks kind of generic and might conflict with the rest of the project settings. Let's add a prefix like OIDC_OP_VERIFY_SSL.

add support for django 1.11

Django 1.11 (LTS) released in April 2017.

This issue covers adding support for it.

That might be as easy as adding relevant bits in the tox.ini.

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.