├── example
├── app
│ ├── __init__.py
│ ├── wsgi.py
│ ├── templates
│ │ ├── oidc_provider
│ │ │ ├── error.html
│ │ │ └── authorize.html
│ │ ├── home.html
│ │ ├── login.html
│ │ └── base.html
│ ├── urls.py
│ └── settings.py
├── .gitignore
├── requirements.txt
├── manage.py
├── Dockerfile
└── README.md
├── oidc_provider
├── __init__.py
├── lib
│ ├── __init__.py
│ ├── utils
│ │ ├── __init__.py
│ │ ├── authorize.py
│ │ ├── sanitization.py
│ │ ├── oauth2.py
│ │ ├── common.py
│ │ └── token.py
│ ├── endpoints
│ │ ├── __init__.py
│ │ └── introspection.py
│ ├── errors.py
│ └── claims.py
├── tests
│ ├── __init__.py
│ ├── app
│ │ ├── __init__.py
│ │ ├── urls.py
│ │ └── utils.py
│ ├── templates
│ │ └── accounts
│ │ │ ├── logout.html
│ │ │ └── login.html
│ ├── cases
│ │ ├── test_commands.py
│ │ ├── test_settings.py
│ │ ├── test_middleware.py
│ │ ├── test_provider_info_endpoint.py
│ │ ├── test_claims.py
│ │ ├── test_admin.py
│ │ ├── test_userinfo_endpoint.py
│ │ └── test_introspection_endpoint.py
│ └── settings.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── creatersakey.py
├── migrations
│ ├── __init__.py
│ ├── 0006_unique_user_client.py
│ ├── 0004_remove_userinfo.py
│ ├── 0003_code_nonce.py
│ ├── 0027_alter_rsakey_options.py
│ ├── 0005_token_refresh_token.py
│ ├── 0010_code_is_authentication.py
│ ├── 0012_auto_20160405_2041.py
│ ├── 0019_auto_20161005_1552.py
│ ├── 0021_refresh_token_not_unique.py
│ ├── 0009_auto_20160202_1945.py
│ ├── 0014_client_jwt_alg.py
│ ├── 0024_auto_20180327_1959.py
│ ├── 0013_auto_20160407_1912.py
│ ├── 0020_client__post_logout_redirect_uris.py
│ ├── 0008_rsakey.py
│ ├── 0011_client_client_type.py
│ ├── 0023_client_owner.py
│ ├── 0025_user_field_codetoken.py
│ ├── 0022_auto_20170331_1626.py
│ ├── 0002_userconsent.py
│ ├── 0007_auto_20160111_1844.py
│ ├── 0018_hybridflow_and_clientattrs.py
│ ├── 0017_auto_20160811_1954.py
│ ├── 0015_change_client_code.py
│ ├── 0026_client_multiple_response_types.py
│ └── 0001_initial.py
├── version.py
├── templates
│ └── oidc_provider
│ │ ├── error.html
│ │ ├── end_session_completed.html
│ │ ├── end_session_failed.html
│ │ ├── end_session_prompt.html
│ │ ├── authorize.html
│ │ ├── hidden_inputs.html
│ │ └── check_session_iframe.html
├── locale
│ ├── es
│ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ ├── fr
│ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ ├── pl
│ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ ├── ru
│ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ └── zh_Hans
│ │ └── LC_MESSAGES
│ │ ├── django.mo
│ │ └── django.po
├── signals.py
├── compat.py
├── apps.py
├── middleware.py
├── urls.py
├── static
│ └── oidc_provider
│ │ └── js
│ │ └── sha256.min.js
├── admin.py
└── settings.py
├── docs
├── .gitignore
├── requirements.txt
├── images
│ ├── add_rsa_key.png
│ └── client_creation.png
├── sections
│ ├── signals.rst
│ ├── userconsent.rst
│ ├── serverkeys.rst
│ ├── installation.rst
│ ├── contribute.rst
│ ├── templates.rst
│ ├── tokenintrospection.rst
│ ├── oauth2.rst
│ ├── accesstokens.rst
│ ├── examples.rst
│ ├── relyingparties.rst
│ ├── sessionmanagement.rst
│ └── scopesclaims.rst
└── index.rst
├── .github
├── FUNDING.yml
└── workflows
│ └── main.yml
├── pyproject.toml
├── .gitignore
├── MANIFEST.in
├── .vscode
└── settings.json
├── .readthedocs.yaml
├── tox.ini
├── LICENSE
├── README.md
└── setup.py
/example/app/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/oidc_provider/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _build/
2 |
3 |
--------------------------------------------------------------------------------
/oidc_provider/lib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/oidc_provider/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/oidc_provider/lib/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/oidc_provider/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/oidc_provider/tests/app/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: juanifioren
2 |
--------------------------------------------------------------------------------
/oidc_provider/lib/endpoints/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx
2 | sphinx_rtd_theme
--------------------------------------------------------------------------------
/oidc_provider/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/oidc_provider/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.9.0"
2 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite3
2 | *.pem
3 | static/
4 |
--------------------------------------------------------------------------------
/oidc_provider/templates/oidc_provider/error.html:
--------------------------------------------------------------------------------
1 |
{{ error }}
2 | {{ description }}
--------------------------------------------------------------------------------
/example/requirements.txt:
--------------------------------------------------------------------------------
1 | django==4.2
2 | https://github.com/juanifioren/django-oidc-provider/archive/master.zip
3 |
--------------------------------------------------------------------------------
/docs/images/add_rsa_key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juanifioren/django-oidc-provider/HEAD/docs/images/add_rsa_key.png
--------------------------------------------------------------------------------
/docs/images/client_creation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juanifioren/django-oidc-provider/HEAD/docs/images/client_creation.png
--------------------------------------------------------------------------------
/oidc_provider/templates/oidc_provider/end_session_completed.html:
--------------------------------------------------------------------------------
1 | End Session Completed
2 |
3 | You've been logged out.
--------------------------------------------------------------------------------
/oidc_provider/templates/oidc_provider/end_session_failed.html:
--------------------------------------------------------------------------------
1 | End Session Failed
2 |
3 | You can now close this window.
--------------------------------------------------------------------------------
/oidc_provider/tests/templates/accounts/logout.html:
--------------------------------------------------------------------------------
1 | Bye!
2 | Thanks for spending some quality time with the web site today.
--------------------------------------------------------------------------------
/oidc_provider/locale/es/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juanifioren/django-oidc-provider/HEAD/oidc_provider/locale/es/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/oidc_provider/locale/fr/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juanifioren/django-oidc-provider/HEAD/oidc_provider/locale/fr/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/oidc_provider/locale/pl/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juanifioren/django-oidc-provider/HEAD/oidc_provider/locale/pl/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/oidc_provider/locale/ru/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juanifioren/django-oidc-provider/HEAD/oidc_provider/locale/ru/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/oidc_provider/locale/zh_Hans/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juanifioren/django-oidc-provider/HEAD/oidc_provider/locale/zh_Hans/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/oidc_provider/signals.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.dispatch import Signal
3 |
4 | user_accept_consent = Signal()
5 | user_decline_consent = Signal()
6 |
--------------------------------------------------------------------------------
/oidc_provider/compat.py:
--------------------------------------------------------------------------------
1 | def get_attr_or_callable(obj, name):
2 | target = getattr(obj, name)
3 | if callable(target):
4 | return target()
5 | return target
6 |
--------------------------------------------------------------------------------
/example/app/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.core.wsgi import get_wsgi_application
4 |
5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
6 |
7 | application = get_wsgi_application()
8 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.ruff]
2 | line-length = 100
3 |
4 | [tool.ruff.lint]
5 | select = [
6 | # Pyflakes
7 | "F",
8 | # isort
9 | "I",
10 | ]
11 |
12 | [tool.ruff.lint.isort]
13 | force-single-line = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | build/
3 | dist/
4 | *.py[cod]
5 | *.egg-info
6 | .ropeproject
7 | .tox
8 | .coverage
9 | src/
10 | .venv
11 | .idea
12 | docs/_build/
13 | .eggs/
14 | .python-version
15 | .pytest_cache/
16 | .coverage*
17 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.md
3 | recursive-include oidc_provider/static *
4 | recursive-include oidc_provider/templates *
5 | recursive-include oidc_provider/tests/templates *
6 | recursive-include oidc_provider/locale *
7 |
--------------------------------------------------------------------------------
/oidc_provider/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class OIDCProviderConfig(AppConfig):
6 | name = "oidc_provider"
7 | verbose_name = _("OpenID Connect Provider")
8 |
--------------------------------------------------------------------------------
/oidc_provider/tests/templates/accounts/login.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/example/app/templates/oidc_provider/error.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
{{ error }}
8 |
{{ description }}
9 |
10 |
11 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/oidc_provider/templates/oidc_provider/end_session_prompt.html:
--------------------------------------------------------------------------------
1 | End Session
2 |
3 | Hi {{ user.email }} , are you sure you want to log out{% if client %} from {{ client.name }} app{% endif %}?
4 |
5 |
13 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0006_unique_user_client.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("oidc_provider", "0005_token_refresh_token"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterUniqueTogether(
14 | name="userconsent",
15 | unique_together=set([("user", "client")]),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[python]": {
3 | "editor.formatOnSave": true,
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.ruff": "explicit",
6 | "source.organizeImports.ruff": "explicit"
7 | },
8 | "editor.defaultFormatter": "charliermarsh.ruff"
9 | },
10 | "ruff.enable": true,
11 | "ruff.nativeServer": true,
12 | "python.analysis.ignore": ["*"],
13 | "python.analysis.autoImportCompletions": false,
14 | "pylint.enabled": false
15 | }
--------------------------------------------------------------------------------
/oidc_provider/migrations/0004_remove_userinfo.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("oidc_provider", "0003_code_nonce"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name="userinfo",
15 | name="user",
16 | ),
17 | migrations.DeleteModel(
18 | name="UserInfo",
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0003_code_nonce.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations
5 | from django.db import models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("oidc_provider", "0002_userconsent"),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name="code",
16 | name="nonce",
17 | field=models.CharField(default=b"", max_length=255, blank=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0027_alter_rsakey_options.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2024-10-03 19:10
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("oidc_provider", "0026_client_multiple_response_types"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterModelOptions(
13 | name="rsakey",
14 | options={
15 | "ordering": ["id"],
16 | "verbose_name": "RSA Key",
17 | "verbose_name_plural": "RSA Keys",
18 | },
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0005_token_refresh_token.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations
5 | from django.db import models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("oidc_provider", "0004_remove_userinfo"),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name="token",
16 | name="refresh_token",
17 | field=models.CharField(max_length=255, unique=True, null=True),
18 | preserve_default=True,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0010_code_is_authentication.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9 on 2016-02-16 20:32
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0009_auto_20160202_1945"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="code",
17 | name="is_authentication",
18 | field=models.BooleanField(default=False),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/oidc_provider/tests/cases/test_commands.py:
--------------------------------------------------------------------------------
1 | from io import StringIO
2 |
3 | from django.core.management import call_command
4 | from django.test import TestCase
5 |
6 |
7 | class CommandsTest(TestCase):
8 | def test_creatersakey_output(self):
9 | out = StringIO()
10 | call_command("creatersakey", stdout=out)
11 | self.assertIn("RSA key successfully created", out.getvalue())
12 |
13 | def test_makemigrations_output(self):
14 | out = StringIO()
15 | call_command("makemigrations", "oidc_provider", stdout=out)
16 | self.assertIn("No changes detected in app", out.getvalue())
17 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0012_auto_20160405_2041.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9 on 2016-04-05 20:41
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0011_client_client_type"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name="client",
17 | name="client_secret",
18 | field=models.CharField(blank=True, default=b"", max_length=255),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/oidc_provider/templates/oidc_provider/authorize.html:
--------------------------------------------------------------------------------
1 | Request for Permission
2 |
3 | Client {{ client.name }} would like to access this information of you ...
4 |
5 |
21 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0019_auto_20161005_1552.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.2 on 2016-10-05 15:52
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0018_hybridflow_and_clientattrs"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name="client",
17 | name="client_secret",
18 | field=models.CharField(blank=True, max_length=255, verbose_name="Client SECRET"),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file for Sphinx projects
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.10"
12 |
13 | # Build documentation in the "docs/" directory with Sphinx
14 | sphinx:
15 | configuration: docs/conf.py
16 |
17 | # Optionally build your docs in additional formats such as PDF and ePub
18 | formats:
19 | - pdf
20 |
21 | # Python requirements required to build your documentation
22 | python:
23 | install:
24 | - requirements: docs/requirements.txt
--------------------------------------------------------------------------------
/example/app/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.auth import views as auth_views
3 | from django.urls import include
4 | from django.urls import re_path
5 | from django.views.generic import TemplateView
6 |
7 | urlpatterns = [
8 | re_path(r"^$", TemplateView.as_view(template_name="home.html"), name="home"),
9 | re_path(
10 | r"^accounts/login/$", auth_views.LoginView.as_view(template_name="login.html"), name="login"
11 | ), # noqa
12 | re_path(r"^accounts/logout/$", auth_views.LogoutView.as_view(next_page="/"), name="logout"),
13 | re_path(r"^", include("oidc_provider.urls", namespace="oidc_provider")),
14 | re_path(r"^admin/", admin.site.urls),
15 | ]
16 |
--------------------------------------------------------------------------------
/example/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-slim
2 |
3 | WORKDIR /usr/src/app
4 |
5 | # Copy requirements and install dependencies
6 | COPY requirements.txt .
7 | RUN pip install --upgrade pip && \
8 | pip install --no-cache-dir -r requirements.txt
9 |
10 | # Copy application code
11 | COPY . .
12 |
13 | RUN [ "python", "manage.py", "migrate" ]
14 | RUN [ "python", "manage.py", "creatersakey" ]
15 |
16 | # Create superuser with admin:admin credentials
17 | ENV DJANGO_SUPERUSER_USERNAME=admin
18 | ENV DJANGO_SUPERUSER_EMAIL=admin@example.com
19 | ENV DJANGO_SUPERUSER_PASSWORD=admin
20 | RUN [ "python", "manage.py", "createsuperuser", "--noinput" ]
21 |
22 | EXPOSE 8000
23 | CMD [ "python", "manage.py", "runserver", "0.0.0.0:8000" ]
24 |
--------------------------------------------------------------------------------
/docs/sections/signals.rst:
--------------------------------------------------------------------------------
1 | .. _signals:
2 |
3 | Signals
4 | #######
5 |
6 | Use signals in your application to get notified when some actions occur.
7 |
8 | For example::
9 |
10 | from django.dispatch import receiver
11 |
12 | from oidc_provider.signals import user_decline_consent
13 |
14 |
15 | @receiver(user_decline_consent)
16 | def my_callback(sender, **kwargs):
17 | print(kwargs)
18 | print('Ups! Some user has declined the consent.')
19 |
20 | user_accept_consent
21 | ===================
22 |
23 | Sent when a user accept the authorization page for some client.
24 |
25 | user_decline_consent
26 | ====================
27 |
28 | Sent when a user decline the authorization page for some client.
29 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0021_refresh_token_not_unique.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10 on 2016-12-12 19:44
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0020_client__post_logout_redirect_uris"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name="token",
17 | name="refresh_token",
18 | field=models.CharField(
19 | default="", max_length=255, unique=True, verbose_name="Refresh Token"
20 | ),
21 | preserve_default=False,
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0009_auto_20160202_1945.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9 on 2016-02-02 19:45
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0008_rsakey"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterModelOptions(
16 | name="rsakey",
17 | options={"verbose_name": "RSA Key", "verbose_name_plural": "RSA Keys"},
18 | ),
19 | migrations.AlterField(
20 | model_name="rsakey",
21 | name="key",
22 | field=models.TextField(help_text="Paste your private RSA Key here."),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/oidc_provider/templates/oidc_provider/hidden_inputs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% if params.nonce %} {% endif %}
7 | {% if params.code_challenge %} {% endif %}
8 | {% if params.code_challenge_method %} {% endif %}
9 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0014_client_jwt_alg.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9 on 2016-04-25 18:02
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0013_auto_20160407_1912"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="client",
17 | name="jwt_alg",
18 | field=models.CharField(
19 | choices=[(b"HS256", b"HS256"), (b"RS256", b"RS256")],
20 | default=b"RS256",
21 | max_length=10,
22 | verbose_name="JWT Algorithm",
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0024_auto_20180327_1959.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.3 on 2018-03-27 19:59
2 |
3 | from django.db import migrations
4 | from django.db import models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("oidc_provider", "0023_client_owner"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="client",
15 | name="reuse_consent",
16 | field=models.BooleanField(
17 | default=True,
18 | help_text="If enabled, server will save the user consent given to a specific client, so that user won't be prompted for the same authorization multiple times.",
19 | verbose_name="Reuse Consent?",
20 | ),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0013_auto_20160407_1912.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9 on 2016-04-07 19:12
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0012_auto_20160405_2041"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="code",
17 | name="code_challenge",
18 | field=models.CharField(max_length=255, null=True),
19 | ),
20 | migrations.AddField(
21 | model_name="code",
22 | name="code_challenge_method",
23 | field=models.CharField(max_length=255, null=True),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/example/app/templates/home.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n static %}
3 |
4 | {% block content %}
5 |
6 |
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Example Project
2 |
3 | On this example you'll be running your own OIDC provider in a second. This is a Django app with all the necessary things to work with `django-oidc-provider` package.
4 |
5 | ## Setup & running using Docker
6 |
7 | Build and run the container.
8 |
9 | ```bash
10 | $ docker build -t django-oidc-provider .
11 | $ docker run -p 8000:8000 --name django-oidc-provider-app django-oidc-provider
12 | ```
13 |
14 | Go to http://localhost:8000/ and create your Client.
15 |
16 | ## Install package for development
17 |
18 | After you run `pip install -r requirements.txt`.
19 | ```bash
20 | # Remove pypi package.
21 | $ pip uninstall django-oidc-provider
22 |
23 | # Go back to django-oidc-provider/ folder and add the package on editable mode.
24 | $ cd ..
25 | $ pip install -e .
26 | ```
27 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0020_client__post_logout_redirect_uris.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9.7 on 2016-11-01 14:59
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0019_auto_20161005_1552"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="client",
17 | name="_post_logout_redirect_uris",
18 | field=models.TextField(
19 | blank=True,
20 | default="",
21 | help_text="Enter each URI on a new line.",
22 | verbose_name="Post Logout Redirect URIs",
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0008_rsakey.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9 on 2016-01-25 17:48
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0007_auto_20160111_1844"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="RSAKey",
17 | fields=[
18 | (
19 | "id",
20 | models.AutoField(
21 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
22 | ),
23 | ),
24 | ("key", models.TextField()),
25 | ],
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/docs/sections/userconsent.rst:
--------------------------------------------------------------------------------
1 | .. _userconsent:
2 |
3 | User Consent
4 | ############
5 |
6 | The package store some information after the user grant access to some client. For example, you can use the ``UserConsent`` model to list applications that the user have authorized access. Like Google does `here `_.
7 |
8 | >>> from oidc_provider.models import UserConsent
9 | >>> UserConsent.objects.filter(user__email='some@email.com')
10 | []
11 |
12 | Note: the ``UserConsent`` model is not included in the admin.
13 |
14 |
15 | Properties
16 | ==========
17 |
18 | * ``user``: Django user object.
19 | * ``client``: Relying Party object.
20 | * ``expires_at``: Expiration date of the consent.
21 | * ``scope``: Scopes authorized.
22 | * ``date_given``: Date of the authorization.
23 |
--------------------------------------------------------------------------------
/oidc_provider/lib/utils/authorize.py:
--------------------------------------------------------------------------------
1 | try:
2 | from urllib import urlencode
3 |
4 | from urlparse import parse_qs
5 | from urlparse import urlsplit
6 | from urlparse import urlunsplit
7 | except ImportError:
8 | from urllib.parse import parse_qs
9 | from urllib.parse import urlencode
10 | from urllib.parse import urlsplit
11 | from urllib.parse import urlunsplit
12 |
13 |
14 | def strip_prompt_login(path):
15 | """
16 | Strips 'login' from the 'prompt' query parameter.
17 | """
18 | uri = urlsplit(path)
19 | query_params = parse_qs(uri.query)
20 | prompt_list = query_params.get("prompt", "")[0].split()
21 | if "login" in prompt_list:
22 | prompt_list.remove("login")
23 | query_params["prompt"] = " ".join(prompt_list)
24 | if not query_params["prompt"]:
25 | del query_params["prompt"]
26 | uri = uri._replace(query=urlencode(query_params, doseq=True))
27 | return urlunsplit(uri)
28 |
--------------------------------------------------------------------------------
/oidc_provider/middleware.py:
--------------------------------------------------------------------------------
1 | try:
2 | # https://docs.djangoproject.com/en/1.10/topics/http/middleware/#upgrading-pre-django-1-10-style-middleware
3 | from django.utils.deprecation import MiddlewareMixin
4 | except ImportError:
5 | MiddlewareMixin = object
6 |
7 | from oidc_provider import settings
8 | from oidc_provider.lib.utils.common import get_browser_state_or_default
9 |
10 |
11 | class SessionManagementMiddleware(MiddlewareMixin):
12 | """
13 | Maintain a `op_browser_state` cookie along with the `sessionid` cookie that
14 | represents the End-User's login state at the OP. If the user is not logged
15 | in then use the value of settings.OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY.
16 | """
17 |
18 | def process_response(self, request, response):
19 | if settings.get("OIDC_SESSION_MANAGEMENT_ENABLE"):
20 | response.set_cookie("op_browser_state", get_browser_state_or_default(request))
21 | return response
22 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0011_client_client_type.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9 on 2016-04-04 19:56
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0010_code_is_authentication"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="client",
17 | name="client_type",
18 | field=models.CharField(
19 | choices=[(b"confidential", b"Confidential"), (b"public", b"Public")],
20 | default=b"confidential",
21 | help_text="Confidential clients are capable of maintaining the confidentiality of their "
22 | "credentials. Public clients are incapable.",
23 | max_length=30,
24 | ),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/oidc_provider/tests/app/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import views as auth_views
2 |
3 | try:
4 | from django.urls import include
5 | from django.urls import re_path
6 | except ImportError:
7 | from django.conf.urls import include
8 | from django.conf.urls import url as re_path
9 | from django.contrib import admin
10 | from django.views.generic import TemplateView
11 |
12 | urlpatterns = [
13 | re_path(r"^$", TemplateView.as_view(template_name="home.html"), name="home"),
14 | re_path(
15 | r"^accounts/login/$",
16 | auth_views.LoginView.as_view(template_name="accounts/login.html"),
17 | name="login",
18 | ),
19 | re_path(
20 | r"^accounts/logout/$",
21 | auth_views.LogoutView.as_view(template_name="accounts/logout.html"),
22 | name="logout",
23 | ),
24 | re_path(r"^openid/", include("oidc_provider.urls", namespace="oidc_provider")),
25 | re_path(r"^admin/", admin.site.urls),
26 | ]
27 |
--------------------------------------------------------------------------------
/oidc_provider/lib/utils/sanitization.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | def sanitize_client_id(client_id):
5 | """
6 | Sanitize client_id according to OAuth 2.0 RFC 6749 specification.
7 |
8 | Removes control characters that can cause database errors while preserving
9 | all valid visible ASCII characters (VCHAR: 0x21-0x7E) as defined by the
10 | OAuth 2.0 specification.
11 |
12 | Args:
13 | client_id (str): The client_id parameter from the request
14 |
15 | Returns:
16 | str: Sanitized client_id with control characters removed
17 |
18 | Examples:
19 | >>> sanitize_client_id("Hello\\x00World")
20 | 'HelloWorld'
21 | >>> sanitize_client_id("valid-client-123")
22 | 'valid-client-123'
23 | >>> sanitize_client_id("")
24 | ''
25 | >>> sanitize_client_id(None)
26 | ''
27 | """
28 | if not client_id:
29 | return ""
30 |
31 | return re.sub(r"[^\x21-\x7E]", "", client_id)
32 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0023_client_owner.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2017-11-08 21:43
3 | from __future__ import unicode_literals
4 |
5 | import django.db.models.deletion
6 | from django.conf import settings
7 | from django.db import migrations
8 | from django.db import models
9 |
10 |
11 | class Migration(migrations.Migration):
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ("oidc_provider", "0022_auto_20170331_1626"),
15 | ]
16 |
17 | operations = [
18 | migrations.AddField(
19 | model_name="client",
20 | name="owner",
21 | field=models.ForeignKey(
22 | blank=True,
23 | default=None,
24 | null=True,
25 | on_delete=django.db.models.deletion.SET_NULL,
26 | related_name="oidc_clients_set",
27 | to=settings.AUTH_USER_MODEL,
28 | verbose_name="Owner",
29 | ),
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/docs/sections/serverkeys.rst:
--------------------------------------------------------------------------------
1 | .. _serverkeys:
2 |
3 | Server Keys
4 | ###########
5 |
6 | Server RSA keys are used to sign/encrypt ID Tokens. These keys are stored in the ``RSAKey`` model. So the package will automatically generate public keys and expose them in the ``jwks_uri`` endpoint.
7 |
8 | You can easily create them with the admin:
9 |
10 | .. image:: ../images/add_rsa_key.png
11 | :align: center
12 |
13 | Or by using ``python manage.py creatersakey`` command.
14 |
15 | Here is an example response from the ``jwks_uri`` endpoint::
16 |
17 | GET /openid/jwks HTTP/1.1
18 | Host: localhost:8000
19 |
20 | {
21 | "keys":[
22 | {
23 | "use":"sig",
24 | "e":"AQAB",
25 | "kty":"RSA",
26 | "alg":"RS256",
27 | "n":"3Gm0pS7ij_SnY96wkbaki74MUYJrobXecO6xJhvmAEEhMHGpO0m4H2nbOWTf6Jc1FiiSvgvhObVk9xPOM6qMTQ5D5pfWZjNk99qDJXvAE4ImM8S0kCaBJGT6e8JbuDllCUq8aL71t67DhzbnoBsKCnVOE1GJffpMcDdBUYkAsx8",
28 | "kid":"a38ea7fbf944cc060eaf5acc1956b0e3"
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/example/app/templates/oidc_provider/authorize.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n static %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
{% trans 'Request for Permission' %}
9 |
Client {{ client.name }} would like to access this information of you.
10 |
22 |
23 |
24 |
25 | {% endblock %}
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist=
3 | docs,
4 | py38-django{32,42},
5 | py39-django{32,42},
6 | py310-django{32,42,52},
7 | py311-django{42,52},
8 | py312-django{42,52},
9 | py313-django{42,52},
10 | ruff
11 |
12 | [testenv]
13 | changedir=
14 | oidc_provider
15 | deps =
16 | django32: django>=3.2,<3.3
17 | django42: django>=4.2,<4.3
18 | django52: django>=5.2,<5.3
19 | freezegun
20 | psycopg2-binary
21 | pytest
22 | pytest-django
23 | pytest-cov
24 |
25 | commands =
26 | pytest --cov=oidc_provider {posargs}
27 |
28 | [testenv:docs]
29 | basepython = python3.11
30 | changedir = docs
31 | allowlist_externals =
32 | mkdir
33 | deps =
34 | sphinx
35 | sphinx_rtd_theme
36 | commands =
37 | mkdir -p _static/
38 | sphinx-build -v -W -b html -d {envtmpdir}/doctrees -D html_static_path="_static" . {envtmpdir}/html
39 |
40 | [testenv:ruff]
41 | basepython = python3.11
42 | deps =
43 | ruff
44 | commands =
45 | ruff check --diff
46 | ruff format --check --diff
47 |
48 | [pytest]
49 | DJANGO_SETTINGS_MODULE = oidc_provider.tests.settings
50 | python_files = test_*.py
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2024 Juan Ignacio Fiorentino
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0025_user_field_codetoken.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.3 on 2018-04-13 19:34
2 |
3 | import django.db.models.deletion
4 | from django.conf import settings
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0024_auto_20180327_1959"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="client",
17 | name="_scope",
18 | field=models.TextField(
19 | blank=True,
20 | default="",
21 | help_text="Specifies the authorized scope values for the client app.",
22 | verbose_name="Scopes",
23 | ),
24 | ),
25 | migrations.AlterField(
26 | model_name="token",
27 | name="user",
28 | field=models.ForeignKey(
29 | null=True,
30 | on_delete=django.db.models.deletion.CASCADE,
31 | to=settings.AUTH_USER_MODEL,
32 | verbose_name="User",
33 | ),
34 | ),
35 | ]
36 |
--------------------------------------------------------------------------------
/docs/sections/installation.rst:
--------------------------------------------------------------------------------
1 | .. _installation:
2 |
3 | Installation
4 | ############
5 |
6 | Requirements
7 | ============
8 |
9 | * Python: ``3.8`` ``3.9`` ``3.10`` ``3.11`` ``3.12``
10 | * Django: ``3.2`` ``4.2`` ``5.1``
11 |
12 | Quick Installation
13 | ==================
14 |
15 | If you want to get started fast see our ``/example`` folder in your local installation. Or look at it `on github `_.
16 |
17 | Install the package using pip::
18 |
19 | $ pip install django-oidc-provider
20 |
21 | Add it to your apps in your project's django settings::
22 |
23 | INSTALLED_APPS = [
24 | # ...
25 | 'oidc_provider',
26 | # ...
27 | ]
28 |
29 | Include our urls to your project's ``urls.py``::
30 |
31 | urlpatterns = [
32 | # ...
33 | path('openid/', include('oidc_provider.urls', namespace='oidc_provider')),
34 | # ...
35 | ]
36 |
37 | Run the migrations and generate a server RSA key::
38 |
39 | $ python manage.py migrate
40 | $ python manage.py creatersakey
41 |
42 | Add this required variable to your project's django settings::
43 |
44 | LOGIN_URL = '/accounts/login/'
45 |
--------------------------------------------------------------------------------
/oidc_provider/tests/cases/test_settings.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from django.test import override_settings
3 |
4 | from oidc_provider import settings
5 |
6 | CUSTOM_TEMPLATES = {"authorize": "custom/authorize.html", "error": "custom/error.html"}
7 |
8 |
9 | class SettingsTest(TestCase):
10 | @override_settings(OIDC_TEMPLATES=CUSTOM_TEMPLATES)
11 | def test_override_templates(self):
12 | self.assertEqual(settings.get("OIDC_TEMPLATES"), CUSTOM_TEMPLATES)
13 |
14 | def test_unauthenticated_session_management_key_has_default(self):
15 | key = settings.get("OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY")
16 | self.assertRegex(key, r"[a-zA-Z0-9]+")
17 | self.assertGreater(len(key), 50)
18 |
19 | def test_unauthenticated_session_management_key_has_constant_value(self):
20 | key1 = settings.get("OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY")
21 | key2 = settings.get("OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY")
22 | self.assertEqual(key1, key2)
23 |
24 | @override_settings(OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE=False)
25 | def test_can_override_with_false_value(self):
26 | self.assertFalse(settings.get("OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE"))
27 |
--------------------------------------------------------------------------------
/example/app/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 |
4 | {% block content %}
5 |
6 |
28 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0022_auto_20170331_1626.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.6 on 2017-03-31 16:26
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("oidc_provider", "0021_refresh_token_not_unique"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="client",
17 | name="require_consent",
18 | field=models.BooleanField(
19 | default=True,
20 | help_text="If disabled, the Server will NEVER ask the user for consent.",
21 | verbose_name="Require Consent?",
22 | ),
23 | ),
24 | migrations.AddField(
25 | model_name="client",
26 | name="reuse_consent",
27 | field=models.BooleanField(
28 | default=True,
29 | help_text="If enabled, the Server will save the user consent given to a specific client,"
30 | " so that user won't be prompted for the same authorization multiple times.",
31 | verbose_name="Reuse Consent?",
32 | ),
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/oidc_provider/management/commands/creatersakey.py:
--------------------------------------------------------------------------------
1 | from cryptography.hazmat.primitives import serialization
2 | from cryptography.hazmat.primitives.asymmetric import rsa
3 | from django.core.management.base import BaseCommand
4 |
5 | from oidc_provider.models import RSAKey
6 |
7 |
8 | class Command(BaseCommand):
9 | help = "Randomly generate a new RSA key for the OpenID server"
10 |
11 | def handle(self, *args, **options):
12 | try:
13 | # Generate a new RSA private key with 2048 bits
14 | private_key = rsa.generate_private_key(
15 | public_exponent=65537,
16 | key_size=2048,
17 | )
18 |
19 | # Serialize the private key to PEM format
20 | key_pem = private_key.private_bytes(
21 | encoding=serialization.Encoding.PEM,
22 | format=serialization.PrivateFormat.PKCS8,
23 | encryption_algorithm=serialization.NoEncryption(),
24 | ).decode("utf-8")
25 |
26 | rsakey = RSAKey(key=key_pem)
27 | rsakey.save()
28 | self.stdout.write("RSA key successfully created with kid: {0}".format(rsakey.kid))
29 | except Exception as e:
30 | self.stdout.write("Something goes wrong: {0}".format(e))
31 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0002_userconsent.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.conf import settings
5 | from django.db import migrations
6 | from django.db import models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ("oidc_provider", "0001_initial"),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name="UserConsent",
18 | fields=[
19 | (
20 | "id",
21 | models.AutoField(
22 | verbose_name="ID", serialize=False, auto_created=True, primary_key=True
23 | ),
24 | ),
25 | ("expires_at", models.DateTimeField()),
26 | ("_scope", models.TextField(default=b"")),
27 | ("client", models.ForeignKey(to="oidc_provider.Client", on_delete=models.CASCADE)),
28 | ("user", models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
29 | ],
30 | options={
31 | "abstract": False,
32 | },
33 | bases=(models.Model,),
34 | ),
35 | ]
36 |
--------------------------------------------------------------------------------
/oidc_provider/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import re_path
2 | from django.views.decorators.csrf import csrf_exempt
3 |
4 | from oidc_provider import settings
5 | from oidc_provider import views
6 |
7 | app_name = "oidc_provider"
8 | urlpatterns = [
9 | re_path(r"^authorize/?$", views.AuthorizeView.as_view(), name="authorize"),
10 | re_path(r"^token/?$", csrf_exempt(views.TokenView.as_view()), name="token"),
11 | re_path(r"^userinfo/?$", csrf_exempt(views.userinfo), name="userinfo"),
12 | re_path(r"^end-session/?$", views.EndSessionView.as_view(), name="end-session"),
13 | re_path(
14 | r"^end-session-prompt/?$", views.EndSessionPromptView.as_view(), name="end-session-prompt"
15 | ),
16 | re_path(
17 | r"^\.well-known/openid-configuration/?$",
18 | views.ProviderInfoView.as_view(),
19 | name="provider-info",
20 | ),
21 | re_path(r"^introspect/?$", views.TokenIntrospectionView.as_view(), name="token-introspection"),
22 | re_path(r"^jwks/?$", views.JwksView.as_view(), name="jwks"),
23 | ]
24 |
25 | if settings.get("OIDC_SESSION_MANAGEMENT_ENABLE"):
26 | urlpatterns += [
27 | re_path(
28 | r"^check-session-iframe/?$",
29 | views.CheckSessionIframeView.as_view(),
30 | name="check-session-iframe",
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/docs/sections/contribute.rst:
--------------------------------------------------------------------------------
1 | .. _contribute:
2 |
3 | Contribute
4 | ##########
5 |
6 | We love contributions, so please feel free to fix bugs, improve things, provide documentation. These are the steps:
7 |
8 | * Create an issue and explain your feature/bugfix.
9 | * Wait collaborators comments.
10 | * Fork the project and create new branch from ``develop``.
11 | * Make your feature addition or bug fix.
12 | * Add tests and documentation if needed.
13 | * Create pull request for the issue to the ``develop`` branch.
14 | * Wait collaborators reviews.
15 |
16 | Running Tests
17 | =============
18 |
19 | Use `tox `_ for running tests in each of the environments, also to run coverage and flake8 among::
20 |
21 | # Run all tests.
22 | $ tox
23 |
24 | # Run with Python 3.11 and Django 4.2.
25 | $ tox -e py311-django42
26 |
27 | # Run a single test method.
28 | $ tox -e py311-django42 -- tests/cases/test_authorize_endpoint.py::TestClass::test_some_method
29 |
30 | We use `Github Actions `_ to automatically test every commit to the project.
31 |
32 | Improve Documentation
33 | =====================
34 |
35 | We use `Sphinx `_ to generate this documentation. If you want to add or modify something just:
36 |
37 | * Install Sphinx and the auto-build tool (``pip install sphinx sphinx_rtd_theme sphinx-autobuild``).
38 | * Move inside the docs folder. ``cd docs/``
39 | * Generate and watch docs by running ``sphinx-autobuild . _build/``.
40 | * Open ``http://127.0.0.1:8000`` in a browser.
41 |
--------------------------------------------------------------------------------
/oidc_provider/tests/cases/test_middleware.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 | from django.test import TestCase
4 | from django.test import override_settings
5 | from django.urls import re_path
6 | from django.views.generic import View
7 |
8 |
9 | class StubbedViews:
10 | class SampleView(View):
11 | pass
12 |
13 | urlpatterns = [re_path("^test/", SampleView.as_view())]
14 |
15 |
16 | MW_CLASSES = (
17 | "django.contrib.sessions.middleware.SessionMiddleware",
18 | "oidc_provider.middleware.SessionManagementMiddleware",
19 | )
20 |
21 |
22 | @override_settings(
23 | ROOT_URLCONF=StubbedViews,
24 | MIDDLEWARE=MW_CLASSES,
25 | MIDDLEWARE_CLASSES=MW_CLASSES,
26 | OIDC_SESSION_MANAGEMENT_ENABLE=True,
27 | )
28 | class MiddlewareTestCase(TestCase):
29 | def setUp(self):
30 | patcher = patch("oidc_provider.middleware.get_browser_state_or_default")
31 | self.mock_get_state = patcher.start()
32 |
33 | def test_session_management_middleware_sets_cookie_on_response(self):
34 | response = self.client.get("/test/")
35 |
36 | self.assertIn("op_browser_state", response.cookies)
37 | self.assertEqual(
38 | response.cookies["op_browser_state"].value, str(self.mock_get_state.return_value)
39 | )
40 | self.mock_get_state.assert_called_once_with(response.wsgi_request)
41 |
42 | @override_settings(OIDC_SESSION_MANAGEMENT_ENABLE=False)
43 | def test_session_management_middleware_does_not_set_cookie_if_session_management_disabled(self):
44 | response = self.client.get("/test/")
45 |
46 | self.assertNotIn("op_browser_state", response.cookies)
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django OpenID Connect Provider
2 |
3 | [](https://pypi.python.org/pypi/django-oidc-provider)
4 | [](https://pypi.python.org/pypi/django-oidc-provider)
5 | [](https://pypi.python.org/pypi/django-oidc-provider)
6 | [](http://django-oidc-provider.readthedocs.io/)
7 |
8 | ## About OpenID
9 |
10 | OpenID Connect is a simple identity layer on top of the OAuth 2.0 protocol, which allows computing clients to verify the identity of an end-user based on the authentication performed by an authorization server, as well as to obtain basic profile information about the end-user in an interoperable and REST-like manner. Like [Google](https://developers.google.com/identity/protocols/OpenIDConnect) for example.
11 |
12 | ## About the package
13 |
14 | `django-oidc-provider` can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect (and OAuth2) capabilities to your Django projects.
15 |
16 | Support for Python 3 and latest versions of django.
17 |
18 | [Read documentation for more info.](http://django-oidc-provider.readthedocs.org/)
19 |
20 | [Do you want to contribute? Please read this.](http://django-oidc-provider.readthedocs.io/en/master/sections/contribute.html)
21 |
22 | ## Thanks to our sponsors
23 |
24 | [](https://github.com/agilentia)
25 |
--------------------------------------------------------------------------------
/oidc_provider/migrations/0007_auto_20160111_1844.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9 on 2016-01-11 18:44
3 | from __future__ import unicode_literals
4 |
5 | import datetime
6 |
7 | from django.db import migrations
8 | from django.db import models
9 |
10 |
11 | class Migration(migrations.Migration):
12 | dependencies = [
13 | ("oidc_provider", "0006_unique_user_client"),
14 | ]
15 |
16 | operations = [
17 | migrations.AlterModelOptions(
18 | name="client",
19 | options={"verbose_name": "Client", "verbose_name_plural": "Clients"},
20 | ),
21 | migrations.AlterModelOptions(
22 | name="code",
23 | options={
24 | "verbose_name": "Authorization Code",
25 | "verbose_name_plural": "Authorization Codes",
26 | },
27 | ),
28 | migrations.AlterModelOptions(
29 | name="token",
30 | options={"verbose_name": "Token", "verbose_name_plural": "Tokens"},
31 | ),
32 | migrations.AddField(
33 | model_name="client",
34 | name="date_created",
35 | field=models.DateField(
36 | auto_now_add=True,
37 | default=datetime.datetime(
38 | 2016, 1, 11, 18, 44, 32, 192477, tzinfo=datetime.timezone.utc
39 | ),
40 | ),
41 | preserve_default=False,
42 | ),
43 | migrations.AlterField(
44 | model_name="client",
45 | name="_redirect_uris",
46 | field=models.TextField(
47 | default=b"", help_text="Enter each URI on a new line.", verbose_name="Redirect URI"
48 | ),
49 | ),
50 | ]
51 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from setuptools import find_packages
4 | from setuptools import setup
5 |
6 | version = {}
7 | with open("./oidc_provider/version.py") as fp:
8 | exec(fp.read(), version)
9 |
10 | # allow setup.py to be run from any path
11 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
12 |
13 | setup(
14 | name="django-oidc-provider",
15 | version=version["__version__"],
16 | packages=find_packages(),
17 | include_package_data=True,
18 | license="MIT License",
19 | description="OpenID Connect Provider implementation for Django.",
20 | long_description="http://github.com/juanifioren/django-oidc-provider",
21 | url="http://github.com/juanifioren/django-oidc-provider",
22 | author="Juan Ignacio Fiorentino",
23 | author_email="juanifioren@gmail.com",
24 | zip_safe=False,
25 | classifiers=[
26 | "Development Status :: 5 - Production/Stable",
27 | "Environment :: Web Environment",
28 | "Framework :: Django",
29 | "Intended Audience :: Developers",
30 | "License :: OSI Approved :: MIT License",
31 | "Operating System :: OS Independent",
32 | "Programming Language :: Python",
33 | "Programming Language :: Python :: 3",
34 | "Programming Language :: Python :: 3.8",
35 | "Programming Language :: Python :: 3.9",
36 | "Programming Language :: Python :: 3.10",
37 | "Programming Language :: Python :: 3.11",
38 | "Programming Language :: Python :: 3.12",
39 | "Programming Language :: Python :: 3.13",
40 | "Topic :: Internet :: WWW/HTTP",
41 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
42 | ],
43 | test_suite="runtests.runtests",
44 | tests_require=[
45 | "PyJWT>=2.8.0",
46 | "cryptography>=3.4.0",
47 | ],
48 | install_requires=[
49 | "PyJWT>=2.8.0",
50 | "cryptography>=3.4.0",
51 | ],
52 | )
53 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: "CI"
2 | on:
3 | push:
4 | branches: ["master", "develop"]
5 | pull_request:
6 |
7 | concurrency:
8 | group: check-${{ github.ref }}
9 | cancel-in-progress: true
10 |
11 | jobs:
12 | formatting:
13 | name: "Check Code Formatting"
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: astral-sh/ruff-action@v3
18 | with:
19 | args: "--version"
20 | - run: "ruff format --check --diff"
21 |
22 | linting:
23 | name: "Check Code Linting"
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v4
27 | - uses: astral-sh/ruff-action@v3
28 | with:
29 | args: "--version"
30 | - run: "ruff check --diff"
31 |
32 | test_matrix_prep:
33 | name: "Prepare Test Matrix"
34 | runs-on: ubuntu-latest
35 | outputs:
36 | matrix: "${{ steps.set-matrix.outputs.matrix }}"
37 | steps:
38 | - uses: actions/checkout@v4
39 | - uses: astral-sh/setup-uv@v3
40 | - run: uv tool install tox
41 | - id: set-matrix
42 | run: |
43 | matrix=$(tox -l | jq -Rc 'select(test("^py\\d+.*django\\d+")) | capture("^py(?\\d+).*django(?\\d+)") | {"python": (.python | tostring | .[0:1] + "." + .[1:]), "django": (.django | tostring | .[0:1] + "." + .[1:])}' | jq -sc '{include: .}')
44 | echo "matrix=$matrix" >> $GITHUB_OUTPUT
45 |
46 | test:
47 | name: "Test Django ${{ matrix.django }} | Python ${{ matrix.python }}"
48 | needs: test_matrix_prep
49 | runs-on: ubuntu-latest
50 | strategy:
51 | fail-fast: false
52 | matrix: ${{ fromJson(needs.test_matrix_prep.outputs.matrix) }}
53 | steps:
54 | - uses: actions/checkout@v4
55 | - uses: astral-sh/setup-uv@v3
56 | - run: uv tool install tox
57 | - uses: actions/setup-python@v4
58 | with:
59 | python-version: ${{ matrix.python }}
60 | - name: Run tox
61 | run: tox run --skip-missing-interpreters=false -e py$(echo "${{ matrix.python }}" | tr -d '.')-django$(echo "${{ matrix.django }}" | tr -d '.')
62 |
--------------------------------------------------------------------------------
/oidc_provider/templates/oidc_provider/check_session_iframe.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 | OP Iframe
7 |
8 |
49 |
50 |
51 | OpenID Connect Session Management OP Iframe.
52 |
53 |
54 |
--------------------------------------------------------------------------------
/oidc_provider/tests/settings.py:
--------------------------------------------------------------------------------
1 | DEBUG = False
2 |
3 | SECRET_KEY = "this-should-be-top-secret"
4 |
5 | DATABASES = {
6 | "default": {
7 | "ENGINE": "django.db.backends.sqlite3",
8 | "NAME": ":memory:",
9 | }
10 | }
11 |
12 | SITE_ID = 1
13 |
14 | MIDDLEWARE_CLASSES = [
15 | "django.middleware.common.CommonMiddleware",
16 | "django.contrib.sessions.middleware.SessionMiddleware",
17 | "django.contrib.auth.middleware.AuthenticationMiddleware",
18 | ]
19 |
20 | MIDDLEWARE = [
21 | "django.middleware.common.CommonMiddleware",
22 | "django.contrib.sessions.middleware.SessionMiddleware",
23 | "django.contrib.auth.middleware.AuthenticationMiddleware",
24 | ]
25 |
26 | TEMPLATES = [
27 | {
28 | "BACKEND": "django.template.backends.django.DjangoTemplates",
29 | "DIRS": [],
30 | "APP_DIRS": True,
31 | "OPTIONS": {
32 | "context_processors": [
33 | "django.template.context_processors.debug",
34 | "django.template.context_processors.request",
35 | "django.contrib.auth.context_processors.auth",
36 | "django.contrib.messages.context_processors.messages",
37 | ],
38 | },
39 | },
40 | ]
41 |
42 | INSTALLED_APPS = [
43 | "django.contrib.auth",
44 | "django.contrib.contenttypes",
45 | "django.contrib.sessions",
46 | "django.contrib.sites",
47 | "django.contrib.messages",
48 | "django.contrib.admin",
49 | "oidc_provider",
50 | ]
51 |
52 | ROOT_URLCONF = "oidc_provider.tests.app.urls"
53 |
54 | TEMPLATE_DIRS = [
55 | "oidc_provider/tests/templates",
56 | ]
57 |
58 | USE_TZ = True
59 |
60 | LOGGING = {
61 | "version": 1,
62 | "disable_existing_loggers": False,
63 | "handlers": {
64 | "console": {
65 | "class": "logging.StreamHandler",
66 | },
67 | },
68 | "loggers": {
69 | "oidc_provider": {
70 | "handlers": ["console"],
71 | "level": "DEBUG",
72 | },
73 | },
74 | }
75 |
76 | # OIDC Provider settings.
77 |
78 | SITE_URL = "http://localhost:8000"
79 | OIDC_USERINFO = "oidc_provider.tests.app.utils.userinfo"
80 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Welcome to Django OIDC Provider Documentation!
2 | ==============================================
3 |
4 | This tiny (but powerful!) package can help you to provide out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. And as a side effect a fair implementation of OAuth2.0 too. Covers Authorization Code, Implicit and Hybrid flows.
5 |
6 | Also implements the following specifications:
7 |
8 | * `OpenID Connect Discovery 1.0 `_
9 | * `OpenID Connect Session Management 1.0 `_
10 | * `OAuth 2.0 for Native Apps `_
11 | * `OAuth 2.0 Resource Owner Password Credentials Grant `_
12 | * `Proof Key for Code Exchange by OAuth Public Clients `_
13 |
14 | --------------------------------------------------------------------------------
15 |
16 | Before getting started there are some important things that you should know:
17 |
18 | * Despite that implementation MUST support TLS, you *can* make request without using SSL. There is no control on that.
19 | * Supports only requesting Claims using Scope values, so you cannot request individual Claims.
20 | * If you enable the Resource Owner Password Credentials Grant, you MUST implement protection against brute force attacks on the token endpoint
21 |
22 | --------------------------------------------------------------------------------
23 |
24 | Contents:
25 |
26 | .. toctree::
27 | :maxdepth: 2
28 |
29 | sections/installation
30 | sections/relyingparties
31 | sections/serverkeys
32 | sections/templates
33 | sections/scopesclaims
34 | sections/userconsent
35 | sections/oauth2
36 | sections/accesstokens
37 | sections/sessionmanagement
38 | sections/tokenintrospection
39 | sections/settings
40 | sections/signals
41 | sections/examples
42 | sections/contribute
43 | sections/changelog
44 | ..
45 |
46 | Indices and tables
47 | ==================
48 |
49 | * :ref:`genindex`
50 | * :ref:`modindex`
51 | * :ref:`search`
52 |
--------------------------------------------------------------------------------
/docs/sections/templates.rst:
--------------------------------------------------------------------------------
1 | .. _templates:
2 |
3 | Templates
4 | #########
5 |
6 | Add your own templates files inside a folder named ``templates/oidc_provider/``.
7 | You can copy the sample html files here and customize them with your own style.
8 |
9 | authorize.html
10 | ==============
11 | ::
12 |
13 | Request for Permission
14 |
15 | Client {{ client.name }} would like to access this information of you ...
16 |
17 |
33 |
34 | error.html
35 | ==========
36 | ::
37 |
38 | {{ error }}
39 | {{ description }}
40 |
41 | You can also customize paths to your custom templates by putting them in ``OIDC_TEMPLATES`` in the settings.
42 |
43 | The following contexts will be passed to the ``authorize`` and ``error`` templates respectively::
44 |
45 | # For authorize template
46 | {
47 | 'client': 'an instance of Client for the auth request',
48 | 'hidden_inputs': 'a rendered html with all the hidden inputs needed for AuthorizeEndpoint',
49 | 'params': 'a dict containing the params in the auth request',
50 | 'scopes': 'a list of scopes'
51 | }
52 |
53 | # For error template
54 | {
55 | 'error': 'string stating the error',
56 | 'description': 'string stating description of the error'
57 | }
58 |
59 | end_session_prompt.html
60 | =======================
61 |
62 | Read more at :doc:`Session Management > Logout consent prompt ` section.
63 |
64 | end_session_completed.html
65 | ==========================
66 |
67 | Read more at :doc:`Session Management > Other scenarios <../sections/sessionmanagement>` section.
68 |
69 | end_session_failed.html
70 | =======================
71 |
72 | Read more at :doc:`Session Management > Other scenarios <../sections/sessionmanagement>` section.
73 |
--------------------------------------------------------------------------------
/oidc_provider/tests/cases/test_provider_info_endpoint.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 | from django.core.cache import cache
4 | from django.test import RequestFactory
5 | from django.test import TestCase
6 | from django.test import override_settings
7 |
8 | try:
9 | from django.urls import reverse
10 | except ImportError:
11 | from django.core.urlresolvers import reverse
12 |
13 | from oidc_provider.views import ProviderInfoView
14 |
15 |
16 | class ProviderInfoTestCase(TestCase):
17 | def setUp(self):
18 | self.factory = RequestFactory()
19 |
20 | def tearDown(self):
21 | cache.clear()
22 |
23 | @patch("oidc_provider.views.ProviderInfoView._build_cache_key")
24 | def test_response(self, build_cache_key):
25 | """
26 | See if the endpoint is returning the corresponding
27 | server information by checking status, content type, etc.
28 | """
29 | url = reverse("oidc_provider:provider-info")
30 |
31 | request = self.factory.get(url)
32 |
33 | response = ProviderInfoView.as_view()(request)
34 |
35 | # Caching not available by default.
36 | build_cache_key.assert_not_called()
37 |
38 | self.assertEqual(response.status_code, 200)
39 | self.assertEqual(response["Content-Type"] == "application/json", True)
40 | self.assertEqual(bool(response.content), True)
41 |
42 | @override_settings(OIDC_DISCOVERY_CACHE_ENABLE=True)
43 | @patch("oidc_provider.views.ProviderInfoView._build_cache_key")
44 | def test_response_with_cache_enabled(self, build_cache_key):
45 | """
46 | Enable caching on the discovery endpoint and ensure data is being saved on cache.
47 | """
48 | build_cache_key.return_value = "key"
49 |
50 | url = reverse("oidc_provider:provider-info")
51 |
52 | request = self.factory.get(url)
53 |
54 | response = ProviderInfoView.as_view()(request)
55 | self.assertEqual(response.status_code, 200)
56 | build_cache_key.assert_called_once()
57 |
58 | assert "authorization_endpoint" in cache.get("key")
59 |
60 | response = ProviderInfoView.as_view()(request)
61 | self.assertEqual(response.status_code, 200)
62 | self.assertEqual(response["Content-Type"] == "application/json", True)
63 | self.assertEqual(bool(response.content), True)
64 |
--------------------------------------------------------------------------------
/docs/sections/tokenintrospection.rst:
--------------------------------------------------------------------------------
1 | .. _tokenintrospection:
2 |
3 | Token Introspection
4 | ###################
5 |
6 | The `OAuth 2.0 Authorization Framework `_ extends its scope with many other speficications. One of these is the `OAuth 2.0 Token Introspection (RFC 7662) `_ which defines a protocol that allows authorized protected resources to query the authorization server to determine the set of metadata for a given token that was presented to them by an OAuth 2.0 client.
7 |
8 | Client Setup
9 | ============
10 | In order to enable this feature, some configurations must be performed in the ``Client``.
11 |
12 | - The scope key:``token_introspection`` must be added to the client's scope.
13 |
14 | If ``OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE`` is set to ``True`` then:
15 |
16 | - The ``client_id`` must be added to the client's scope.
17 |
18 | Introspection Endpoint
19 | ======================
20 | The introspection endpoint ``(/introspect)`` is an OAuth 2.0 endpoint that takes a parameter representing an OAuth 2.0 token and returns a JSON document representing the meta information surrounding the token.
21 |
22 | The introspection endpoint its called using an HTTP POST request with parameters sent as *"application/x-www-form-urlencoded"* and **Basic authentication** (``base64(client_id:client_secret``).
23 |
24 | Parameters:
25 |
26 | * ``token``
27 | REQUIRED. The string value of an ``access_token`` previously issued.
28 |
29 | Example request::
30 |
31 | curl -X POST \
32 | http://localhost:8000/introspect \
33 | -H 'Authorization: Basic NDgwNTQ2OmIxOGIyODVmY2E5N2Fm' \
34 | -H 'Content-Type: application/x-www-form-urlencoded' \
35 | -d token=6dd4b859706944848183d26f2fcb99c6
36 |
37 | Example Response::
38 |
39 | {
40 | "aud": "480546",
41 | "sub": "1",
42 | "exp": 1538971676,
43 | "iat": 1538971076,
44 | "iss": "http://localhost:8000",
45 | "active": true,
46 | "client_id": "480546"
47 | }
48 |
49 | Introspection Endpoint Errors
50 | =============================
51 | In case of error, the Introspection Endpoint will return a JSON document with the key ``active: false``
52 |
53 | Example Error Response::
54 |
55 | {
56 | "active": "false"
57 | }
58 |
--------------------------------------------------------------------------------
/docs/sections/oauth2.rst:
--------------------------------------------------------------------------------
1 | .. _oauth2:
2 |
3 | OAuth2 Server
4 | #############
5 |
6 | Because OIDC is a layer on top of the OAuth 2.0 protocol, this package also gives you a simple but effective OAuth2 server that you can use not only for logging in your users on multiple platforms, but also to protect other resources you want to expose.
7 |
8 | Protecting Views
9 | ================
10 |
11 | Here we are going to protect a view with a scope called ``read_books``::
12 |
13 | from django.http import JsonResponse
14 | from django.views.decorators.http import require_http_methods
15 |
16 | from oidc_provider.lib.utils.oauth2 import protected_resource_view
17 |
18 |
19 | @require_http_methods(['GET'])
20 | @protected_resource_view(['read_books'])
21 | def protected_api(request, *args, **kwargs):
22 |
23 | dic = {
24 | 'protected': 'information',
25 | }
26 |
27 | return JsonResponse(dic, status=200)
28 |
29 | Client Credentials Grant
30 | ========================
31 |
32 | The client can request an access token using only its client credentials (ID and SECRET) when the client is requesting access to the protected resources under its control, that have been previously arranged with the authorization server using the ``client.scope`` field.
33 |
34 | .. note::
35 | You can use Django admin to manually set the client scope or programmatically::
36 |
37 | client.scope = ['read_books', 'add_books']
38 | client.save()
39 |
40 | This is how the request should look like::
41 |
42 | POST /token HTTP/1.1
43 | Host: localhost:8000
44 | Authorization: Basic eWZ3a3c0cWxtaHY0cToyVWE0QjVzRlhmZ3pNeXR5d1FqT01jNUsxYmpWeXhXeXRySVdsTmpQbld3\
45 | Content-Type: application/x-www-form-urlencoded
46 |
47 | grant_type=client_credentials
48 |
49 | A successful access token response will like this::
50 |
51 | HTTP/1.1 200 OK
52 | Content-Type: application/json
53 | Cache-Control: no-store
54 | Pragma: no-cache
55 |
56 | {
57 | "token_type" : "Bearer",
58 | "access_token" : "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJzY3AiOlsib3BlbmlkIiw...",
59 | "expires_in" : 3600,
60 | "scope" : "read_books add_books"
61 | }
62 |
63 | Token introspection can be used to validate access tokens requested with client credentials if the ``OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE`` setting is ``False``.
64 |
--------------------------------------------------------------------------------
/example/app/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load i18n static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {% trans 'OpenID Provider Example' %}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
36 |
37 |
38 |
39 | {% block content %}{% endblock %}
40 |
41 |
42 |
43 |
44 |
45 |
46 |