├── .coveragerc
├── .gitignore
├── .travis.yml
├── DockerfileDocs
├── LICENSE
├── Makefile
├── README.rst
├── docs
├── Makefile
├── _static
│ └── .gitkeep
├── _templates
│ └── .gitkeep
├── add-realm.png
├── add-server.png
├── clear_client_tokens.png
├── conf.py
├── index.rst
├── keycloak_manage_clients.png
├── keycloak_remote_resource_management.png
├── keycloak_resources.png
├── keycloak_scopes.png
├── refresh_certificates.png
├── refresh_well_known.png
├── requirements.txt
├── scenario
│ ├── example_project.rst
│ ├── initial_setup.rst
│ ├── local_user_setup.rst
│ ├── migrating.rst
│ ├── multi_tenancy.rst
│ ├── permissions_by_resources_and_scopes.rst
│ ├── permissions_by_roles.rst
│ └── remote_user_setup.rst
├── synchronize_permissions.png
└── synchronize_resources.png
├── example
├── README.md
├── docker-compose.yml
├── keycloak
│ └── export
│ │ ├── example-realm.json
│ │ ├── example-users-0.json
│ │ ├── master-realm.json
│ │ └── master-users-0.json
├── nginx
│ ├── certs
│ │ ├── README.md
│ │ ├── ca.key
│ │ ├── ca.pem
│ │ ├── localhost.yarf.nl.cert
│ │ ├── localhost.yarf.nl.csr
│ │ ├── localhost.yarf.nl.key
│ │ └── ssl.ext
│ └── conf.d
│ │ ├── identity.localhost.yarf.nl.conf
│ │ ├── resource-provider-api.localhost.yarf.nl.conf
│ │ └── resource-provider.localhost.yarf.nl.conf
├── resource-provider-api
│ ├── Dockerfile
│ ├── __init__.py
│ ├── docker
│ │ └── entrypoint-dev.sh
│ ├── manage.py
│ ├── myapp
│ │ ├── __init__.py
│ │ ├── apps.py
│ │ ├── fixtures
│ │ │ ├── 0001_admin_user.py
│ │ │ ├── 0002_server.py
│ │ │ ├── 0003_realm.py
│ │ │ ├── 0004_client.py
│ │ │ └── __init__.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ ├── views.py
│ │ └── wsgi.py
│ └── requirements.txt
└── resource-provider
│ ├── Dockerfile
│ ├── __init__.py
│ ├── docker
│ └── entrypoint-dev.sh
│ ├── manage.py
│ ├── myapp
│ ├── __init__.py
│ ├── apps.py
│ ├── fixtures
│ │ ├── 0001_admin_user.py
│ │ ├── 0002_server.py
│ │ ├── 0003_realm.py
│ │ ├── 0004_client.py
│ │ └── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_auto_20180322_2059.py
│ │ └── __init__.py
│ ├── models.py
│ ├── settings.py
│ ├── templates
│ │ ├── 403.html
│ │ └── myapp
│ │ │ ├── base.html
│ │ │ ├── home.html
│ │ │ ├── permission.html
│ │ │ └── secured.html
│ ├── urls.py
│ ├── views.py
│ └── wsgi.py
│ └── requirements.txt
├── pytest.ini
├── setup.cfg
├── setup.py
├── sonar-project.properties
└── src
└── django_keycloak
├── __init__.py
├── admin
├── __init__.py
├── realm.py
└── server.py
├── app_settings.py
├── apps.py
├── auth
├── __init__.py
└── backends.py
├── factories.py
├── hashers.py
├── management
├── __init__.py
└── commands
│ ├── __init__.py
│ ├── keycloak_add_user.py
│ ├── keycloak_refresh_realm.py
│ └── keycloak_sync_resources.py
├── middleware.py
├── migrations
├── 0001_initial.py
├── 0002_auto_20180322_2059.py
├── 0003_auto_20190204_1949.py
├── 0004_client_service_account_profile.py
├── 0005_auto_20190219_2002.py
├── 0006_remove_client_service_account.py
└── __init__.py
├── models.py
├── remote_user.py
├── response.py
├── services
├── __init__.py
├── client.py
├── exceptions.py
├── oidc_profile.py
├── permissions.py
├── realm.py
├── remote_client.py
├── uma.py
└── users.py
├── templates
└── django_keycloak
│ ├── includes
│ └── session_iframe_support.html
│ └── session_iframe.html
├── tests
├── __init__.py
├── backends
│ ├── __init__.py
│ └── keycloak_authorization_base
│ │ ├── __init__.py
│ │ ├── test_get_keycloak_permissions.py
│ │ └── test_has_perm.py
├── mixins.py
├── services
│ ├── __init__.py
│ ├── oidc_profile
│ │ ├── __init__.py
│ │ ├── test_get_active_access_token.py
│ │ ├── test_get_entitlement.py
│ │ ├── test_get_or_create_from_id_token.py
│ │ └── test_update_or_create.py
│ └── realm
│ │ ├── __init__.py
│ │ ├── test_get_realm_api_client.py
│ │ └── test_refresh_well_known_oidc.py
└── settings.py
├── urls.py
└── views.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | */tests/*
4 | src/django_keycloak/factories.py
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 | .spyproject
93 |
94 | # Rope project settings
95 | .ropeproject
96 |
97 | # mkdocs documentation
98 | /site
99 |
100 | # mypy
101 | .mypy_cache/
102 |
103 | .idea
104 | db.sqlite3
105 | .pytest_cache
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.7"
4 | - "3.4"
5 | - "3.5"
6 | env:
7 | - DJANGO_VERSION=1.11
8 | - DJANGO_VERSION=2.0
9 | addons:
10 | sonarcloud:
11 | organization: $SC_ORG
12 | token: $SC_TOKEN
13 |
14 | matrix:
15 | exclude:
16 | - python: "3.3"
17 | env: DJANGO_VERSION=1.11
18 | - python: "2.7"
19 | env: DJANGO_VERSION=2.0
20 | before_install:
21 | - pip install -U pip
22 | - pip install -q Django==$DJANGO_VERSION
23 | - pip install -U wheel setuptools flake8
24 | install:
25 | - python setup.py -q install
26 | script:
27 | - flake8 ./src
28 | - python setup.py test --addopts "--cov=django_keycloak --cov-report xml:coverage.xml"
29 | - sonar-scanner
30 |
31 | cache:
32 | directories:
33 | - '$HOME/.sonar/cache'
34 |
35 | after_success:
36 | - pip install codecov
37 | - codecov
38 |
--------------------------------------------------------------------------------
/DockerfileDocs:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | # Install required packages
4 | RUN apk --no-cache add \
5 | py2-pip \
6 | python-dev \
7 | make \
8 | git \
9 | gcc \
10 | alpine-sdk
11 |
12 | RUN mkdir -p /src
13 | WORKDIR /src
14 | ADD . /src
15 |
16 | RUN pip install -r docs/requirements.txt
17 | RUN pip install sphinx-autobuild
18 | RUN pip install -e .
19 |
20 | EXPOSE 8050
21 |
22 | CMD sphinx-autobuild --host 0.0.0.0 --port 8050 -z src docs docs/_build/html
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Peter Slump
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install-python:
2 | pip install --upgrade setuptools
3 | pip install -e .
4 | pip install "file://`pwd`#egg=django-keycloak[dev,doc]"
5 |
6 | bump-patch:
7 | bumpversion patch
8 |
9 | bump-minor:
10 | bumpversion minor
11 |
12 | deploy-pypi: clear
13 | python3 -c "import sys; sys.version_info >= (3, 5, 3) or sys.stdout.write('Python version must be greatest then 3.5.2\n') or exit(1)"
14 | python3 setup.py sdist bdist_wheel
15 | twine upload dist/*
16 |
17 | clear:
18 | rm -rf dist/*
19 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ===============
2 | Django Keycloak
3 | ===============
4 |
5 | .. image:: https://www.travis-ci.org/Peter-Slump/django-keycloak.svg?branch=master
6 | :target: https://www.travis-ci.org/Peter-Slump/django-keycloak
7 | :alt: Build Status
8 | .. image:: https://readthedocs.org/projects/django-keycloak/badge/?version=latest
9 | :target: http://django-keycloak.readthedocs.io/en/latest/?badge=latest
10 | :alt: Documentation Status
11 | .. image:: https://codecov.io/gh/Peter-Slump/django-keycloak/branch/master/graph/badge.svg
12 | :target: https://codecov.io/gh/Peter-Slump/django-keycloak
13 | :alt: codecov
14 | .. image:: https://api.codeclimate.com/v1/badges/eb19f47dc03dec40cea7/maintainability
15 | :target: https://codeclimate.com/github/Peter-Slump/django-keycloak/maintainability
16 | :alt: Maintainability
17 |
18 | Django app to add Keycloak support to your project.
19 |
20 | `Read documentation `_
21 |
22 | http://www.keycloak.org/
23 |
24 | An showcase/demo project is added in the `example folder `_.
25 |
26 | Development
27 | ===========
28 |
29 | Install development environment:
30 |
31 | .. code:: bash
32 |
33 | $ make install-python
34 |
35 | ------------
36 | Writing docs
37 | ------------
38 |
39 | Documentation is written using Sphinx and maintained in the docs folder.
40 |
41 | To make it easy to write docs Docker support is available.
42 |
43 | First build the Docker container:
44 |
45 | .. code:: bash
46 |
47 | $ docker build . -f DockerfileDocs -t django-keycloak-docs
48 |
49 | Run the container
50 |
51 | .. code:: bash
52 |
53 | $ docker run -v `pwd`:/src --rm -t -i -p 8050:8050 django-keycloak-docs
54 |
55 | Go in the browser to http://localhost:8050 and view the documentation which get
56 | refreshed and updated on every update in the documentation source.
57 |
58 | --------------
59 | Create release
60 | --------------
61 |
62 | .. code:: bash
63 |
64 | $ git checkout master
65 | $ git pull
66 | $ bumpversion release
67 | $ make deploy-pypi
68 | $ bumpversion --no-tag patch
69 | $ git push origin master --tags
70 |
71 | Release Notes
72 | =============
73 |
74 | **unreleased**
75 |
76 | **v0.1.2-dev**
77 |
78 | **v0.1.1**
79 |
80 | * Added support for remote user. Handling identities without registering a User
81 | model. (thanks to `bossan `_)
82 | * Addes support for permissions using resources and scopes.
83 | (thanks to `bossan `_)
84 | * Added example project.
85 | * Updated documentation.
86 |
87 | **v0.1.0**
88 |
89 | * Correctly extract email field name on UserModel (thanks to `swist `_)
90 | * Add support for Oauth2 Token Exchange to exchange tokens with remote clients.
91 | Handy when using multiple applications with different clients which have to
92 | communicate with each other.
93 | * Support for session iframe
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = DjangoKeycloak
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/_static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/_static/.gitkeep
--------------------------------------------------------------------------------
/docs/_templates/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/_templates/.gitkeep
--------------------------------------------------------------------------------
/docs/add-realm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/add-realm.png
--------------------------------------------------------------------------------
/docs/add-server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/add-server.png
--------------------------------------------------------------------------------
/docs/clear_client_tokens.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/clear_client_tokens.png
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Configuration file for the Sphinx documentation builder.
4 | #
5 | # This file does only contain a selection of the most common options. For a
6 | # full list see the documentation:
7 | # http://www.sphinx-doc.org/en/stable/config
8 |
9 | # -- Path setup --------------------------------------------------------------
10 |
11 | # If extensions (or modules to document with autodoc) are in another directory,
12 | # add these directories to sys.path here. If the directory is relative to the
13 | # documentation root, use os.path.abspath to make it absolute, like shown here.
14 | #
15 | # import os
16 | # import sys
17 | # sys.path.insert(0, os.path.abspath('.'))
18 |
19 |
20 | # -- Project information -----------------------------------------------------
21 |
22 | project = u'Django Keycloak'
23 | copyright = u'2018, Peter Slump'
24 | author = u'Peter Slump'
25 |
26 | # The short X.Y version
27 | version = u''
28 | # The full version, including alpha/beta/rc tags
29 | release = u'0.1.2-dev'
30 |
31 |
32 | # -- General configuration ---------------------------------------------------
33 |
34 | # If your documentation needs a minimal Sphinx version, state it here.
35 | #
36 | # needs_sphinx = '1.0'
37 |
38 | # Add any Sphinx extension module names here, as strings. They can be
39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
40 | # ones.
41 | extensions = [
42 | 'sphinx.ext.autodoc',
43 | 'sphinx.ext.intersphinx',
44 | ]
45 |
46 | # Add any paths that contain templates here, relative to this directory.
47 | templates_path = ['_templates']
48 |
49 | # The suffix(es) of source filenames.
50 | # You can specify multiple suffix as a list of string:
51 | #
52 | # source_suffix = ['.rst', '.md']
53 | source_suffix = '.rst'
54 |
55 | # The master toctree document.
56 | master_doc = 'index'
57 |
58 | # The language for content autogenerated by Sphinx. Refer to documentation
59 | # for a list of supported languages.
60 | #
61 | # This is also used if you do content translation via gettext catalogs.
62 | # Usually you set "language" from the command line for these cases.
63 | language = None
64 |
65 | # List of patterns, relative to source directory, that match files and
66 | # directories to ignore when looking for source files.
67 | # This pattern also affects html_static_path and html_extra_path .
68 | exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
69 |
70 | # The name of the Pygments (syntax highlighting) style to use.
71 | pygments_style = 'sphinx'
72 |
73 |
74 | # -- Options for HTML output -------------------------------------------------
75 |
76 | # The theme to use for HTML and HTML Help pages. See the documentation for
77 | # a list of builtin themes.
78 | #
79 | html_theme = 'sphinx_rtd_theme'
80 |
81 | # Theme options are theme-specific and customize the look and feel of a theme
82 | # further. For a list of options available for each theme, see the
83 | # documentation.
84 | #
85 | # html_theme_options = {}
86 |
87 | # Add any paths that contain custom static files (such as style sheets) here,
88 | # relative to this directory. They are copied after the builtin static files,
89 | # so a file named "default.css" will overwrite the builtin "default.css".
90 | html_static_path = ['_static']
91 |
92 | # Custom sidebar templates, must be a dictionary that maps document names
93 | # to template names.
94 | #
95 | # The default sidebars (for documents that don't match any pattern) are
96 | # defined by theme itself. Builtin themes are using these templates by
97 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
98 | # 'searchbox.html']``.
99 | #
100 | # html_sidebars = {}
101 |
102 |
103 | # -- Options for HTMLHelp output ---------------------------------------------
104 |
105 | # Output file base name for HTML help builder.
106 | htmlhelp_basename = 'DjangoKeycloakdoc'
107 |
108 |
109 | # -- Options for LaTeX output ------------------------------------------------
110 |
111 | latex_elements = {
112 | # The paper size ('letterpaper' or 'a4paper').
113 | #
114 | # 'papersize': 'letterpaper',
115 |
116 | # The font size ('10pt', '11pt' or '12pt').
117 | #
118 | # 'pointsize': '10pt',
119 |
120 | # Additional stuff for the LaTeX preamble.
121 | #
122 | # 'preamble': '',
123 |
124 | # Latex figure (float) alignment
125 | #
126 | # 'figure_align': 'htbp',
127 | }
128 |
129 | # Grouping the document tree into LaTeX files. List of tuples
130 | # (source start file, target name, title,
131 | # author, documentclass [howto, manual, or own class]).
132 | latex_documents = [
133 | (master_doc, 'DjangoKeycloak.tex', u'Django Keycloak Documentation',
134 | u'Peter Slump', 'manual'),
135 | ]
136 |
137 |
138 | # -- Options for manual page output ------------------------------------------
139 |
140 | # One entry per manual page. List of tuples
141 | # (source start file, name, description, authors, manual section).
142 | man_pages = [
143 | (master_doc, 'djangokeycloak', u'Django Keycloak Documentation',
144 | [author], 1)
145 | ]
146 |
147 |
148 | # -- Options for Texinfo output ----------------------------------------------
149 |
150 | # Grouping the document tree into Texinfo files. List of tuples
151 | # (source start file, target name, title, author,
152 | # dir menu entry, description, category)
153 | texinfo_documents = [
154 | (master_doc, 'DjangoKeycloak', u'Django Keycloak Documentation',
155 | author, 'DjangoKeycloak', 'One line description of project.',
156 | 'Miscellaneous'),
157 | ]
158 |
159 |
160 | # -- Extension configuration -------------------------------------------------
161 |
162 | # -- Options for intersphinx extension ---------------------------------------
163 |
164 | # Example configuration for intersphinx: refer to the Python standard library.
165 | intersphinx_mapping = {'https://docs.python.org/': None}
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | ===========================================
2 | Welcome to Django Keycloak's documentation!
3 | ===========================================
4 |
5 | .. toctree::
6 | :hidden:
7 | :caption: Scenario's
8 | :maxdepth: 2
9 |
10 | scenario/example_project
11 | scenario/local_user_setup
12 | scenario/remote_user_setup
13 | scenario/initial_setup
14 | scenario/migrating
15 | scenario/permissions_by_roles
16 | scenario/permissions_by_resources_and_scopes
17 | scenario/multi_tenancy
18 |
19 | Django Keycloak adds Keycloak support to your Django project. It's build on top
20 | of `Django's authentication system `_.
21 | It works side-by-side with the standard Django authentication implementation and
22 | has tools to migrate your current users and permissions to Keycloak.
23 |
24 | Features
25 | ========
26 |
27 | - Multi tenancy support
28 | - Permissions by roles or by resource/scope
29 | - Choose if you want to create a local User model for every logged in identity or not.
30 |
31 | Read :ref:`example_project` to quickly test this project.
32 |
33 | .. note:: The documentation and the example project are all based on
34 | Keycloak version 3.4 since that is the latest version which is commercially
35 | supported by Red Hat (SSO).
36 |
37 | Installation
38 | ============
39 |
40 | Install requirement.
41 |
42 | .. code-block:: bash
43 |
44 | $ pip install git+https://github.com/Peter-Slump/django-keycloak.git
45 |
46 | Setup
47 | =====
48 |
49 | Some settings are always required and some other settings are dependant on how
50 | you want to integrate Keycloak in your project.
51 |
52 | Add `django-keycloak` to your installed apps, add the authentication back-end,
53 | add the middleware, configure the urls and point to the correct login page.
54 |
55 | .. code-block:: python
56 |
57 | # your-project/settings.py
58 | INSTALLED_APPS = [
59 | ....
60 |
61 | 'django_keycloak.apps.KeycloakAppConfig'
62 | ]
63 |
64 | MIDDLEWARE = [
65 | ...
66 |
67 | 'django_keycloak.middleware.BaseKeycloakMiddleware',
68 | ]
69 |
70 | AUTHENTICATION_BACKENDS = [
71 | ...
72 |
73 | 'django_keycloak.auth.backends.KeycloakAuthorizationCodeBackend',
74 | ]
75 |
76 | LOGIN_URL = 'keycloak_login'
77 |
78 | .. code-block:: python
79 |
80 | # your-project/urls.py
81 | ...
82 |
83 | urlpatterns = [
84 | ...
85 |
86 | url(r'^keycloak/', include('django_keycloak.urls')),
87 | ]
88 |
89 |
90 | Before you actually start using Django Keycloak make an educated choice between
91 | :ref:`local_user_setup` and :ref:`remote_user_setup`.
92 |
93 | Then walk through the :ref:`initial_setup` to found out how to link your
94 | Keycloak instance to your Django project.
95 |
96 | If you don't want to take all that effort please read about :ref:`example_project`
97 |
98 | Usage
99 | =====
100 |
101 | For requiring a logged in user you can just use the `standard Django
102 | functionality `_.
103 | This also counts for `enforcing permissions `_.
104 |
105 | This app makes use of the `Python Keycloak client `_
106 |
--------------------------------------------------------------------------------
/docs/keycloak_manage_clients.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/keycloak_manage_clients.png
--------------------------------------------------------------------------------
/docs/keycloak_remote_resource_management.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/keycloak_remote_resource_management.png
--------------------------------------------------------------------------------
/docs/keycloak_resources.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/keycloak_resources.png
--------------------------------------------------------------------------------
/docs/keycloak_scopes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/keycloak_scopes.png
--------------------------------------------------------------------------------
/docs/refresh_certificates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/refresh_certificates.png
--------------------------------------------------------------------------------
/docs/refresh_well_known.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/refresh_well_known.png
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | docutils
2 | Jinja2
3 | MarkupSafe
4 | Pygments
5 | Sphinx
6 | sphinx_rtd_theme
7 |
--------------------------------------------------------------------------------
/docs/scenario/example_project.rst:
--------------------------------------------------------------------------------
1 | .. _example_project:
2 |
3 | ================================
4 | Testing with the Example project
5 | ================================
6 |
7 | The quickest way to experiment with this project is by running the example
8 | project. This project is setup using Docker compose.
9 |
10 | Given you have installed Docker and Docker compose run:
11 |
12 | .. code-block:: bash
13 |
14 | $ cd example
15 | $ docker-compose up
16 |
17 | The project exists of a resource provider which mimics a web app and a resource
18 | provider which is only accessible by an API. Next to it is a Keycloak instance
19 | available which is backed by a Postgres database.
20 |
21 | Once you have the containers running you can access it by navigating to:
22 | https://resource-provider.localhost.yarf.nl/ you can login with
23 | username: `testuser` and password: `password`. The admin is
24 | accessible at /admin with username: `admin` and password: `password`.
25 |
26 | The Keycloak instance is available at: https://identity.localhost.yarf.nl/
27 | the username of the admin user is `admin` and the password is `password`.
28 |
29 | The API is available at: https://resource-provider-api.localhost.yarf.nl/
30 | You probably don't actually use this server or only for the admin. The admin is
31 | accessible at /admin with username: `admin` and password: `password`.
32 |
--------------------------------------------------------------------------------
/docs/scenario/initial_setup.rst:
--------------------------------------------------------------------------------
1 | .. _initial_setup:
2 |
3 | =============
4 | Initial setup
5 | =============
6 |
7 | Server configuration
8 | ====================
9 |
10 | First you have to add your Keycloak server. You can do this in the Django Admin.
11 |
12 | .. image:: /add-server.png
13 |
14 | .. note:: When your application access the Keycloak server using a different url
15 | than the public one you can configure this URL as "internal url". Django
16 | Keycloak will use that url for all direct communication but uses the standard
17 | server url to redirect users for authentication.
18 |
19 | Realm configuration
20 | ===================
21 |
22 | After you have created a
23 | `REALM `_
24 | and `Client `_
25 | in Keycloak you can add these in the Django admin.
26 |
27 | .. note:: Django-Keycloak supports multiple realms. However when you configure
28 | multiple realms you have to write your own middleware which selects
29 | the correct realm based on the request. The default middleware always
30 | selects the first realm available in the database.
31 |
32 | .. image:: /add-realm.png
33 |
34 | After you have added the realm please make sure to run te following actions:
35 |
36 | * :ref:`refresh_openid_connect_well_known`
37 | * :ref:`refresh_certificates`
38 | * :ref:`synchronize_permissions` (when using the permission system)
39 |
40 | Tools
41 | =====
42 |
43 | .. _refresh_openid_connect_well_known:
44 |
45 | ----------------------------------
46 | Refresh OpenID Connect .well-known
47 | ----------------------------------
48 |
49 | In the Django Admin you can apply the action "Refresh OpenID Connect
50 | .well-known" for a realm. This retrieves the
51 | `.well-known `_
52 | content for the OpenID Connect functionality and caches this in the database. In
53 | this way it's not required to fetch this file before each request regarding
54 | OpenID Connect to the Keycloak server.
55 |
56 | .. image:: /refresh_well_known.png
57 |
58 | .. _refresh_certificates:
59 |
60 | --------------------
61 | Refresh Certificates
62 | --------------------
63 |
64 | This refreshes the cached certificates from the Keycloak server. These
65 | certificates are used for valiation of the JWT's.
66 |
67 | .. image:: /refresh_certificates.png
68 |
69 | -------------------
70 | Clear client tokens
71 | -------------------
72 |
73 | While debugging client service account permissions it's sometimes required to
74 | refresh te session in order to fetch the new permissions. This can be done with
75 | this action in the Django admin.
76 |
77 | .. image:: /clear_client_tokens.png
78 |
--------------------------------------------------------------------------------
/docs/scenario/local_user_setup.rst:
--------------------------------------------------------------------------------
1 | .. _local_user_setup:
2 |
3 | ============================
4 | Setup for local user storage
5 | ============================
6 |
7 | .. toctree::
8 | :maxdepth: 4
9 |
10 | By local user storage a
11 | `User object `_
12 | get created for every logged in identity. This can be handy when you want to
13 | link objects to this User. If that's not the case please read the
14 | scenario :ref:`remote_user_setup`.
15 |
16 | Since **this is the default behaviour for Django Keycloak** you don't have to
17 | configure any setting.
18 |
19 | Important to point out the `KEYCLOAK_OIDC_PROFILE_MODEL` setting. This should
20 | contain `django_keycloak.OpenIdConnectProfile` (which is the case by default).
21 | The model to store the Open ID Connect profile is a swappable model. When
22 | configured to this model a foreign key to the configured Django User model is
23 | available.
24 |
25 | .. code-block:: python
26 |
27 | # settings.py
28 | KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.OpenIdConnectProfile'
--------------------------------------------------------------------------------
/docs/scenario/migrating.rst:
--------------------------------------------------------------------------------
1 | ======================================
2 | Migrating from Django Auth to Keycloak
3 | ======================================
4 |
5 | There are some tools available which can help by migrating a running project to
6 | Keycloak.
7 |
8 | --------
9 | Add user
10 | --------
11 |
12 | A management command is available to create a Keycloak user based on a local
13 | one.
14 |
15 | .. code:: bash
16 |
17 | $ python manage.py keycloak_add_user --realm --username
18 |
19 | .. note:: In theory it would be possible to synchronize (hashed) passwords to
20 | Keycloak however Keycloak uses a 512 bit hash for pbkdf2_sha256 hashed
21 | passwords, Django generates a 256 bits hash. In that way passwords will not
22 | work when they are copied to Keycloak. The project includes a sha512 hasher
23 | (:class:`django_keycloak.hashers.PBKDF2SHA512PasswordHasher`) which you can
24 | configure to hash passwords in a Keycloak-complient way.
25 |
26 | .. code:: python
27 |
28 | # your-project/settings.py
29 | PASSWORD_HASHERS = [
30 | 'django_keycloak.hashers.PBKDF2SHA512PasswordHasher',
31 | ]
32 |
33 |
34 | .. _synchronize_permissions:
35 |
36 |
--------------------------------------------------------------------------------
/docs/scenario/multi_tenancy.rst:
--------------------------------------------------------------------------------
1 | =============
2 | Multi-tenancy
3 | =============
4 |
5 | Django Keycloak supports multi tenancy by supporting multiple realms. The way to
6 | determine the currently active realm is set to the request in the middleware.
7 | In the project there is currently one middleware available. This is
8 | `django_keycloak.middleware.BaseKeycloakMiddleware`. This middleware add the
9 | first found Realm model to the request object.
10 |
11 | .. code-block:: python
12 |
13 | # your-project/settings.py
14 | MIDDLEWARE = [
15 | ...
16 |
17 | 'django_keycloak.middleware.BaseKeycloakMiddleware',
18 | ]
19 |
20 | If you want to support multiple reams you have to create your own middleware.
21 | There are several methods to determine the currently active realm. You can think
22 | of realm determination by:
23 |
24 | - Hostname
25 | - Environment variable
26 | - Selection during login
27 | - etc.
28 |
29 | It's up to you how the realm get determined and therefore it's also up to
30 | you to `write a proper middleware `_
31 | for it. The only think the middleware has to make the correct Realm model to the
32 | request as `request.realm`. This middleware has to be configured above other
33 | middlewares which have to be configured for authentication purposes.
--------------------------------------------------------------------------------
/docs/scenario/permissions_by_resources_and_scopes.rst:
--------------------------------------------------------------------------------
1 | ===================================
2 | Permissions by resources and scopes
3 | ===================================
4 |
5 | Next to :ref:`permission_by_roles` you can also implement permissions by syncing
6 | Django models as resources in Keycloak and the
7 | `default permissions `_
8 | in Django as scopes in Keycloak.
9 |
10 | Setup
11 | =====
12 |
13 | To configure Django Keycloak to make use of the Resource / Scope method of
14 | permission assigning add the following setting:
15 |
16 | .. code-block:: python
17 |
18 | # your-project/settings.py
19 | KEYCLOAK_PERMISSIONS_METHOD = 'resource'
20 |
21 |
22 | Synchronization
23 | ===============
24 |
25 | In Keycloak enable "Remote Resource Management" for the client:
26 |
27 | .. image:: /keycloak_remote_resource_management.png
28 |
29 | You can use the Django Admin action "Synchronize models as Keycloak resources"
30 | to synchronize models and scopes to Keycloak.
31 |
32 | .. image:: /synchronize_resources.png
33 |
34 | An alternative is to run the Django management command `keycloak_sync_resources`:
35 |
36 | .. code-block:: bash
37 |
38 | $ python manage.py keycloak_sync_resources
39 |
40 | Optionally you can supply a client to which the resources should be synchronized.
41 |
42 | Usage
43 | =====
44 |
45 | After synchronizing you can find the the models as resources and the default permissions as scopes:
46 |
47 | Resources:
48 |
49 | .. image:: /keycloak_resources.png
50 |
51 | Scopes:
52 |
53 | .. image:: /keycloak_scopes.png
54 |
55 | From here you are able to configure your `policies` and `permissions` and assign
56 | them to `users` of `groups` using `roles` in Keycloak. Once assigned you get
57 | them back as permissions in Django where the policies are combined with the
58 | resources just like you are used to in the default Django permission system
59 | i.e. `foo.add_bar` or `foo.change_bar`.
60 |
--------------------------------------------------------------------------------
/docs/scenario/permissions_by_roles.rst:
--------------------------------------------------------------------------------
1 | .. _permission_by_roles:
2 |
3 | ====================
4 | Permissions by roles
5 | ====================
6 |
7 | There are two ways of using permissions one by roles and the other one by
8 | resources/scopes. The roles method is the default one. In this method the
9 | available client roles are available as permissions in your Django Project.
10 |
11 | .. note:: Please read :ref:`synchronize_permissions` if you want to synchronize
12 | all available permissions in your current project to roles in Keycloak.
13 |
14 | Setup
15 | =====
16 |
17 | Since this is the default method of handling permission you don't have to
18 | configure anything. However it's good to know that the
19 | `KEYCLOAK_PERMISSIONS_METHOD` is used to configure the way how permissions are
20 | interpreted.
21 |
22 | .. code-block:: python
23 |
24 | # your-project/settings.py
25 | KEYCLOAK_PERMISSIONS_METHOD = 'role'
26 |
27 |
28 | Synchronize
29 | ===========
30 |
31 | This Django Admin action which can be triggered for a realm synchronizes all
32 | available permission to Keycloak. In keycloak the permissions will get
33 | registered as roles. These roles can be added to a user.
34 |
35 | For this feature the service account should have the
36 | realm-management/manage-clients role assigned.
37 |
38 | .. image:: /keycloak_manage_clients.png
39 |
40 | This only makes sense when you use the `roles` permission method. You can read
41 | about this at scenario: :ref:`permission_by_roles`.
--------------------------------------------------------------------------------
/docs/scenario/remote_user_setup.rst:
--------------------------------------------------------------------------------
1 | .. _remote_user_setup:
2 |
3 | =====================
4 | Setup for remote user
5 | =====================
6 |
7 | It's not required to create a local User object for every logged in identity.
8 | If you don't need a local user object you can setup the app to work with a
9 | remote user. This user behaves like Django's User object but it is not a real
10 | one.
11 |
12 | .. note:: For logging purposes Django admin only works with User objects which
13 | are stored in the database. So you cannot use this method to authenticate
14 | users for admin usage.
15 |
16 | .. warning:: Set the configuration setting below before running the migrations!
17 |
18 | Set the OIDC Profile model to the remote variant:
19 |
20 | .. code-block:: python
21 |
22 | # your-project/settings.py
23 | KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.RemoteUserOpenIdConnectProfile'
24 |
25 | Configure the remote user middleware:
26 |
27 | .. code-block:: python
28 |
29 | MIDDLEWARE = [
30 | ...
31 |
32 | 'django_keycloak.middleware.BaseKeycloakMiddleware',
33 | 'django_keycloak.middleware.RemoteUserAuthenticationMiddleware',
34 | ]
35 |
36 | By default the class `django_keycloak.remote_user.KeycloakRemoteUser` is used as
37 | user, this one will be available on the request when authenticated and will be
38 | returned when you access `RemoteUserOpenIdConnectProfile.user`. If you want
39 | another class (i.e. you need extra properties) you can configure this class
40 | using the setting `KEYCLOAK_REMOTE_USER_MODEL`:
41 |
42 | .. code-block:: python
43 |
44 | KEYCLOAK_REMOTE_USER_MODEL = 'django_keycloak.remote_user.KeycloakRemoteUser'
--------------------------------------------------------------------------------
/docs/synchronize_permissions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/synchronize_permissions.png
--------------------------------------------------------------------------------
/docs/synchronize_resources.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/synchronize_resources.png
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Example project
2 |
3 | This folder contains an example showcase project for the Django Keycloak project.
4 |
5 | It consists out of three applications:
6 | - identity server (Keycloak)
7 | - resource provider (small web application)
8 | - resource provider (API accessible)
9 |
10 | When running the application everything is pre-configured and ready to use.
11 |
12 | Keycloak version 3.4.3-Final is used since that is the latest version which is
13 | commercially supported by Red Hat (SSO).
14 |
15 | You can find the docs for this version here: https://www.keycloak.org/archive/documentation-3.4.html
16 |
17 | You can find the following features in the project:
18 |
19 | - Authentication (login)
20 | - Authorisation (permissions)
21 | - Token Exchange
22 |
23 | ## Run the project
24 |
25 | Installation of Docker and Docker-compose is required
26 |
27 | Run:
28 |
29 | $ docker-compose up
30 |
31 | In your browser visit: https://resource-provider.localhost.yarf.nl/
32 |
33 | Accept the insecure certificate, or add the CA which you can find in `nginx/certs/ca.pem` to your trusted CA's.
34 |
35 | ## Credentials
36 |
37 | Keycloak admin user:
38 |
39 | - username: admin
40 | - password: password
41 |
42 | "Test user" in example Realm
43 |
44 | - username: testuser
45 | - password: password
46 |
47 | Django admin user (Resource Provider & Resource Provider API)
48 |
49 | - username: admin
50 | - password: password
51 |
52 | ## Import/Export
53 |
54 | Using the `command` section in the keycloak configuration in the docker-compose file.
55 | Docs: https://www.keycloak.org/docs/latest/server_admin/index.html#_export_import
56 |
--------------------------------------------------------------------------------
/example/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 |
5 | nginx:
6 | image: nginx:stable-alpine
7 | volumes:
8 | - ./nginx/conf.d:/etc/nginx/conf.d/:ro
9 | - ./nginx/certs:/etc/nginx/certs:ro
10 | networks:
11 | default:
12 | aliases:
13 | - resource-provider.localhost.yarf.nl
14 | - resource-provider-api.localhost.yarf.nl
15 | - identity.localhost.yarf.nl
16 | ports:
17 | - "80:80"
18 | - "443:443"
19 | depends_on:
20 | - keycloak
21 | - resource-provider
22 | - resource-provider-api
23 |
24 | keycloak:
25 | image: jboss/keycloak:3.4.3.Final # Pinned to 3.4.3 because this is currently the latest version which has commercial support from Red Hat: https://www.keycloak.org/support.html
26 | command: [
27 | "-b", "0.0.0.0",
28 | "-Dkeycloak.migration.action=import", # Replace with 'export' in order to export everything
29 | "-Dkeycloak.migration.provider=dir",
30 | "-Dkeycloak.migration.dir=/opt/jboss/keycloak/standalone/configuration/export/",
31 | "-Dkeycloak.migration.strategy=IGNORE_EXISTING"
32 | ]
33 | environment:
34 | - POSTGRES_DATABASE=keycloak
35 | - POSTGRES_USER=keycloak
36 | - POSTGRES_PASSWORD=password
37 | - KEYCLOAK_HOSTNAME=identity.localhost.yarf.nl
38 | # Legacy linking functionality is used
39 | - POSTGRES_PORT_5432_TCP_ADDR=postgres
40 | - POSTGRES_PORT_5432_TCP_PORT=5432
41 | - PROXY_ADDRESS_FORWARDING=true
42 | - KEYCLOAK_LOGLEVEL=DEBUG
43 | # - JAVA_TOOL_OPTIONS=-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled -Dkeycloak.profile.feature.token_exchange=enabled # Required to enable Token exchange feature in newer versions of Keycloak
44 | volumes:
45 | - ./keycloak/export:/opt/jboss/keycloak/standalone/configuration/export
46 | networks:
47 | default:
48 | aliases:
49 | - keycloak
50 |
51 | postgres:
52 | image: postgres:latest
53 | environment:
54 | - POSTGRES_DB=keycloak
55 | - POSTGRES_USER=keycloak
56 | - POSTGRES_PASSWORD=password
57 | networks:
58 | default:
59 | aliases:
60 | - postgres
61 |
62 | resource-provider:
63 | build: ./resource-provider
64 | entrypoint: docker/entrypoint-dev.sh
65 | command: [ "python", "manage.py", "runserver", "0.0.0.0:8001" ]
66 | volumes:
67 | - ./resource-provider:/usr/src/app
68 | - ./../:/usr/src/django-keycloak
69 | - ./../../python-keycloak-client:/usr/src/python-keycloak-client
70 | - ./nginx/certs/ca.pem:/usr/src/ca.pem
71 | networks:
72 | default:
73 | aliases:
74 | - resource-provider
75 |
76 | resource-provider-api:
77 | build: ./resource-provider-api
78 | entrypoint: docker/entrypoint-dev.sh
79 | command: [ "python", "manage.py", "runserver", "0.0.0.0:8002" ]
80 | volumes:
81 | - ./resource-provider-api:/usr/src/app
82 | - ./../:/usr/src/django-keycloak
83 | - ./nginx/certs/ca.pem:/usr/src/ca.pem
84 | networks:
85 | default:
86 | aliases:
87 | - resource-provider-api
--------------------------------------------------------------------------------
/example/keycloak/export/example-users-0.json:
--------------------------------------------------------------------------------
1 | {
2 | "realm" : "example",
3 | "users" : [ {
4 | "id" : "a39d3fb5-6e11-47c5-a48e-50e254804f98",
5 | "createdTimestamp" : 1548797565484,
6 | "username" : "service-account-resource-provider",
7 | "enabled" : true,
8 | "totp" : false,
9 | "emailVerified" : false,
10 | "email" : "service-account-resource-provider@placeholder.org",
11 | "serviceAccountClientId" : "resource-provider",
12 | "credentials" : [ ],
13 | "disableableCredentialTypes" : [ ],
14 | "requiredActions" : [ ],
15 | "realmRoles" : [ "offline_access", "uma_authorization" ],
16 | "clientRoles" : {
17 | "resource-provider" : [ "uma_protection" ],
18 | "realm-management" : [ "manage-clients" ],
19 | "account" : [ "manage-account", "view-profile" ]
20 | },
21 | "notBefore" : 0,
22 | "groups" : [ ]
23 | }, {
24 | "id" : "7ef9e62d-6674-4ca2-9d8c-593ee3519c33",
25 | "createdTimestamp" : 1548797581171,
26 | "username" : "service-account-resource-provider-api",
27 | "enabled" : true,
28 | "totp" : false,
29 | "emailVerified" : false,
30 | "email" : "service-account-resource-provider-api@placeholder.org",
31 | "serviceAccountClientId" : "resource-provider-api",
32 | "credentials" : [ ],
33 | "disableableCredentialTypes" : [ ],
34 | "requiredActions" : [ ],
35 | "realmRoles" : [ "offline_access", "uma_authorization" ],
36 | "clientRoles" : {
37 | "resource-provider-api" : [ "uma_protection" ],
38 | "account" : [ "manage-account", "view-profile" ]
39 | },
40 | "notBefore" : 0,
41 | "groups" : [ ]
42 | }, {
43 | "id" : "483bfd18-32fb-4666-9fed-48cabc376498",
44 | "createdTimestamp" : 1548798984518,
45 | "username" : "testuser",
46 | "enabled" : true,
47 | "totp" : false,
48 | "emailVerified" : true,
49 | "firstName" : "John",
50 | "lastName" : "Doe",
51 | "email" : "john@example.com",
52 | "credentials" : [ {
53 | "type" : "password",
54 | "hashedSaltedValue" : "qhYpBE+XIEwcnZ1Db3UuNlYypzajnn71g6g3nTupnmG0QCAIb8pykNGyumdZB4GFVltCAX0d+m+pEz5zSM+1Ig==",
55 | "salt" : "/ixUwYokI9O3iHeFe30BIA==",
56 | "hashIterations" : 27500,
57 | "counter" : 0,
58 | "algorithm" : "pbkdf2-sha256",
59 | "digits" : 0,
60 | "period" : 0,
61 | "createdDate" : 1548798995962,
62 | "config" : { }
63 | } ],
64 | "disableableCredentialTypes" : [ "password" ],
65 | "requiredActions" : [ ],
66 | "realmRoles" : [ "offline_access", "uma_authorization" ],
67 | "clientRoles" : {
68 | "resource-provider" : [ "some-permission" ],
69 | "realm-management" : [ "manage-users" ],
70 | "account" : [ "manage-account", "view-profile" ]
71 | },
72 | "notBefore" : 0,
73 | "groups" : [ ]
74 | } ]
75 | }
--------------------------------------------------------------------------------
/example/keycloak/export/master-users-0.json:
--------------------------------------------------------------------------------
1 | {
2 | "realm" : "master",
3 | "users" : [ {
4 | "id" : "13376567-82f9-4ff7-bf0e-c74237363105",
5 | "createdTimestamp" : 1548796136436,
6 | "username" : "admin",
7 | "enabled" : true,
8 | "totp" : false,
9 | "emailVerified" : false,
10 | "credentials" : [ {
11 | "type" : "password",
12 | "hashedSaltedValue" : "+NYGHHcl8ScJLcwIOZ5r0jRva/3ht2aWey+vFkNcSyV+gDj9IMqmqlrMM9OobSsNYV2+SnHhrS0Z6ZWswv3CYQ==",
13 | "salt" : "lD4zbiXX8PFYe9/FKDP+0w==",
14 | "hashIterations" : 27500,
15 | "counter" : 0,
16 | "algorithm" : "pbkdf2-sha256",
17 | "digits" : 0,
18 | "period" : 0,
19 | "config" : { }
20 | } ],
21 | "disableableCredentialTypes" : [ "password" ],
22 | "requiredActions" : [ ],
23 | "realmRoles" : [ "admin", "uma_authorization", "offline_access" ],
24 | "clientRoles" : {
25 | "account" : [ "manage-account", "view-profile" ]
26 | },
27 | "notBefore" : 0,
28 | "groups" : [ ]
29 | } ]
30 | }
--------------------------------------------------------------------------------
/example/nginx/certs/README.md:
--------------------------------------------------------------------------------
1 | SSL Certificates
2 | ================
3 |
4 | This folder contains a set of very insecure SSL certificates to be used for
5 | running the server in SSL.
6 |
7 | As you can see the private key and everything is here. This is absolutely not
8 | done for anything that smells production ready.
9 |
10 | However with a CA and a SSL certificate we can have an SSL connection between
11 | all servers in this example setup.
12 |
13 | Below a summary of how these certificates where created so I can redo whenever
14 | I need it.
15 |
16 | 1. Create CA key
17 |
18 |
19 | $ openssl genrsa -out ca.key 2048
20 |
21 | 2. Create CA certificate
22 |
23 |
24 | $ openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 -out ca.pem
25 |
26 |
27 | 3. Generate private key and CSR
28 |
29 |
30 | $ openssl req -out localhost.yarf.nl.csr -newkey rsa:2048 -nodes -keyout localhost.yarf.nl.key
31 |
32 |
33 | 4. Sign with the CA certificate
34 |
35 |
36 | $ openssl x509 -req -in localhost.yarf.nl.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out localhost.yarf.nl.cert -days 1825 -sha256 -extfile ssl.ext
37 |
38 |
39 | 5. Verify the certifcate
40 |
41 |
42 | $ openssl x509 -noout -text -in localhost.yarf.nl.cert
43 |
44 |
45 | 6. Add `ca.pem` to your browser's set of trusted CA's
--------------------------------------------------------------------------------
/example/nginx/certs/ca.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpQIBAAKCAQEA1s7NCNqKe4MZEb/Hhhy0nXiFO32GlgJwcaiVZccKXv0XK5pl
3 | vChHPxNdlxjA9n5bD2tak3IluLcsXAknn8/u6hGJwYizol84z0U+bJpn6EoXesVO
4 | TbQxw3ZfwG1y4HdoJPU3EYjzqoO/i0PUOGP8ZdeERJrcjsb21k6qUMGuuP6XXCVd
5 | UVznHjJcbN3pIber+hO5mmqNgJAgemHI+GmPd+2Kr7GpJluOyhfbc+lE445z6ulw
6 | ts50xnrl8PQlufayhIhNIx5xo6QXxvuJPpPIdcpZZHVgD1Fa/Dct7/Fd0hojSUle
7 | fvSrfazjG6UKPYqXUKMAeYvs9WQfB0llPXagrQIDAQABAoIBAQCSfQM8V3T7GAM/
8 | pG6X2fmeLfOGB2uB33X5PMNtXhiHgMeNV8SrVTiJHlbD1QC62J6W3FovlTd+SYR5
9 | 21BnXvEKNR2hmu0N/SwLxaf1fjMAKY7rjaSBvzH0n0RbQQiHZUbjdUqxrZimHyS/
10 | /2i5cA5PEXNFKerhf3QT4B+r5PmXc9+zu5rLftCFeorquNi1bk96eN7xbMftu/n2
11 | Iwb9kV1dSxzMtWURgj2nks3RBXu+xyu6rwv+c9L2RLcratFsYxUgnrz1NC/zDOBE
12 | 4y05yN8d2qx7DNKkBPEXLDyaETbZXo5J4HKwZQxWcbAh9we/M1TXk1jXVFaB7Xrq
13 | hSYfukYNAoGBAO8k5Ti7/I2UMVB6mX5Ffys7ThT/JDSfW13zqJctSlOLjcXEjvWq
14 | obsJjLCyEOtlLT59eHxNfglbzahpGtrfn582bR3jBejXCynIFVSGtHogx132E+8E
15 | t2KPs49WMdsO/lmQzVYjGT3DsqWaW2NcTb8Ex/ae1730IJugITHXS5yzAoGBAOXy
16 | yEyjexlprkln6lHcnx604jHMmZKQJ+ntpMGi7rxbledYN8OdIqXjrSShGLu3II3i
17 | pHZjNYwd/KChI0mEYK2iqcdre/5wVoj0qqJqLXbzXkLX41YgaQNKB6f7MvPkUSVc
18 | Ik55ywIRB+65fVTL+CNAxVsSsB4g+sNmykIpKD0fAoGAc/jqiJbxpR8mwyaRZWEC
19 | iM6b0SbiQfq97lQJgDbggp98w1nNEmoLQI8jAVV5Sw6n4FQsp+tUoek5VOCTu20T
20 | FbzpMcM0zHPs3/g+D927jDZ1OKXriNA6truFko90Yg1lX74PNiSTSxaqfhDbHNZY
21 | hbgl2P2zFlVbstz2/Bqyem0CgYEAg5/ne8cQjclqlGZBQL6y7pbH642cUsLltgfs
22 | CVNEqNkcA6MBuJ1X2fFriM4WJE18+vrC/Wlom14G38OdOVXnKT01RguGnGydfCPh
23 | ELsKb057pHODlCdVNSbJHySxU95bfLEyig05YWNyUPoofcOLtFI9JhaabYSfRf6u
24 | xBRfDi0CgYEAr4wuKkDhrYZP7eZG4yRVmSULcIR17+FWwKQxbh9PfBHIMG8k8bn0
25 | fqCXDavrS3jRGZSa+fI/1zdRrc6+gN/3Itap84URl/ibD7QPNIBu3sAR0dYAFXih
26 | NwNOR6Zbimh792G21chx3i+548cEKubUsAxzG7875DqTtanB0kCuJ/s=
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/example/nginx/certs/ca.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDkTCCAnmgAwIBAgIUMTe94H1fBJEKrHFgWTMYI8D4kXQwDQYJKoZIhvcNAQEL
3 | BQAwWDELMAkGA1UEBhMCTkwxEzARBgNVBAgMCk92ZXJpanNzZWwxDzANBgNVBAcM
4 | Blp3b2xsZTENMAsGA1UECgwEWWFyZjEUMBIGA1UEAwwLaW5zZWN1cmUuY2EwHhcN
5 | MTkwMTI5MTk0OTI3WhcNMjQwMTI4MTk0OTI3WjBYMQswCQYDVQQGEwJOTDETMBEG
6 | A1UECAwKT3Zlcmlqc3NlbDEPMA0GA1UEBwwGWndvbGxlMQ0wCwYDVQQKDARZYXJm
7 | MRQwEgYDVQQDDAtpbnNlY3VyZS5jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
8 | AQoCggEBANbOzQjainuDGRG/x4YctJ14hTt9hpYCcHGolWXHCl79FyuaZbwoRz8T
9 | XZcYwPZ+Ww9rWpNyJbi3LFwJJ5/P7uoRicGIs6JfOM9FPmyaZ+hKF3rFTk20McN2
10 | X8BtcuB3aCT1NxGI86qDv4tD1Dhj/GXXhESa3I7G9tZOqlDBrrj+l1wlXVFc5x4y
11 | XGzd6SG3q/oTuZpqjYCQIHphyPhpj3ftiq+xqSZbjsoX23PpROOOc+rpcLbOdMZ6
12 | 5fD0Jbn2soSITSMecaOkF8b7iT6TyHXKWWR1YA9RWvw3Le/xXdIaI0lJXn70q32s
13 | 4xulCj2Kl1CjAHmL7PVkHwdJZT12oK0CAwEAAaNTMFEwHQYDVR0OBBYEFFbRJ+tA
14 | iYRKflzljONfTuWL7QSZMB8GA1UdIwQYMBaAFFbRJ+tAiYRKflzljONfTuWL7QSZ
15 | MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHeJK2XeRcCNcY/A
16 | 5nMBG4H+u+FSI3Fc2nXaAlXcXxVaKAxeFrPlis1uZUZ+GQPCJyCKb+nuEPqLMpww
17 | sR7ICoUdUHlq+ciQvY7m2saLw7MI7IXOQl3WymEY1yu/GKPFqPzMMdnn5LOU26nc
18 | U9KtTbcEj23wk0ERFWbk8w2gIDFzmakiaX2S4P/WR2gaXlXGTI3yitMOjg0/sfx4
19 | 0jiMD0vqmk4JVMI76epifJ26Z2ucuTLk9Ea20mKW1VsqUn+6sBIH1uQuh3seoouT
20 | Fg97jLSwnH2sY0vAA8iAKSBewnKgaQQEUEpA3lU0slCIX7Jfioez7wGsL23mLo3R
21 | sB9UL/Q=
22 | -----END CERTIFICATE-----
23 |
--------------------------------------------------------------------------------
/example/nginx/certs/localhost.yarf.nl.cert:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIID9jCCAt6gAwIBAgIUT8ajdKSoj+0agwezVw01d4b4wEIwDQYJKoZIhvcNAQEL
3 | BQAwWDELMAkGA1UEBhMCTkwxEzARBgNVBAgMCk92ZXJpanNzZWwxDzANBgNVBAcM
4 | Blp3b2xsZTENMAsGA1UECgwEWWFyZjEUMBIGA1UEAwwLaW5zZWN1cmUuY2EwHhcN
5 | MTkwMTI5MjAyNTM0WhcNMjQwMTI4MjAyNTM0WjBeMQswCQYDVQQGEwJOTDETMBEG
6 | A1UECAwKT3Zlcmlqc3NlbDEPMA0GA1UEBwwGWndvbGxlMQ0wCwYDVQQKDARZYXJm
7 | MRowGAYDVQQDDBFsb2NhbGhvc3QueWFyZi5ubDCCASIwDQYJKoZIhvcNAQEBBQAD
8 | ggEPADCCAQoCggEBAJ1dCtmkdSHADvqjnD8zRSdJlkFBO/M8MY+dVOqtbVd2yKLH
9 | gNNriGARJy8nvx1c9J8y+ahCJgavunlhVT0Knev9VvlLBuY5wFKVcfOqTnzdcz6s
10 | 1aiwSWD0a5YNheBywPCH2Bf+C19rqNK1MZ1uhkjkgPC7NbIqM5eELPIgkEXOEcBe
11 | Pj1/2phIjG9XCqdfugOgnwBD4wRWW82BbTW4zfdi7ZRLAWVX1mgBb+MNVag3MJv1
12 | KnMPj/1/eU2al9mg28rqXaiB3xyJKvomdKjLJ0zv9Ie9DNSn2RaTj9yZGX2pp3VI
13 | +XzTl950Rf1W2OhjRw2JFdr3JfKo3kEiOB6DuZMCAwEAAaOBsTCBrjAfBgNVHSME
14 | GDAWgBRW0SfrQImESn5c5YzjX07li+0EmTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE
15 | 8DBzBgNVHREEbDBqghppZGVudGl0eS5sb2NhbGhvc3QueWFyZi5ubIIjcmVzb3Vy
16 | Y2UtcHJvdmlkZXIubG9jYWxob3N0LnlhcmYubmyCJ3Jlc291cmNlLXByb3ZpZGVy
17 | LWFwaS5sb2NhbGhvc3QueWFyZi5ubDANBgkqhkiG9w0BAQsFAAOCAQEAJouCHu/r
18 | kKAJYKoKmSRhHdN/JUtYhjnkOCfLrhaFo1l2l1jWEeTU9NH4tA6lH3bhQwxOFvtx
19 | 6ujj17yhHoT/Sdvonw0RR8Iuub8JgolepIi+6yRMFQikwamvaTBs+Cc+PIo1n+bw
20 | qSwLzxIYOovD1beCNt0QIG0GSmfJtFtTijdf6t35vPAWA5sx4WtvAPlTNKHfSTt6
21 | dQGfhPwsrkAIb0ItfowOcnN8ZJFgf1HfV6sy1hX4UaxzsvOgwUEYYtSvjjn3GNvG
22 | 9IuEGX7uM+jGPw3M/aWv0R7WAPvwwOrOwv8bZm7zvgO7H8E297eeKdfLy3oiF9wz
23 | J2CzzhVis5EMTQ==
24 | -----END CERTIFICATE-----
25 |
--------------------------------------------------------------------------------
/example/nginx/certs/localhost.yarf.nl.csr:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE REQUEST-----
2 | MIICozCCAYsCAQAwXjELMAkGA1UEBhMCTkwxEzARBgNVBAgMCk92ZXJpanNzZWwx
3 | DzANBgNVBAcMBlp3b2xsZTENMAsGA1UECgwEWWFyZjEaMBgGA1UEAwwRbG9jYWxo
4 | b3N0LnlhcmYubmwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdXQrZ
5 | pHUhwA76o5w/M0UnSZZBQTvzPDGPnVTqrW1Xdsiix4DTa4hgEScvJ78dXPSfMvmo
6 | QiYGr7p5YVU9Cp3r/Vb5SwbmOcBSlXHzqk583XM+rNWosElg9GuWDYXgcsDwh9gX
7 | /gtfa6jStTGdboZI5IDwuzWyKjOXhCzyIJBFzhHAXj49f9qYSIxvVwqnX7oDoJ8A
8 | Q+MEVlvNgW01uM33Yu2USwFlV9ZoAW/jDVWoNzCb9SpzD4/9f3lNmpfZoNvK6l2o
9 | gd8ciSr6JnSoyydM7/SHvQzUp9kWk4/cmRl9qad1SPl805fedEX9VtjoY0cNiRXa
10 | 9yXyqN5BIjgeg7mTAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAV6mAduvBgGf7
11 | b70w3mwVj4iVC1z+vhvB5icLWFZ4O0yXFNvlI8pILtUXPox7SNkb8Nlc6vVelWCL
12 | /FsdGTc5m50VnKSxKzw0PEj4VqjI9Pkj7qgGrC1A0Iz234EGsu/x+ycwM+J/qfzm
13 | VwZEDlI1SvpVjUgtFfm/9zYENO63yXrwcKmogZvmGyrxIOQNEZJ4Y7KlqPfg9Tmr
14 | LMmw+FUOBXU7LI255BYb2J9WRJ9udGv6iPRhyMch/gfQm6OBJx1FNUCBNTjeynWg
15 | YtNYmmVMMMqLnNOQa0J9xS68nKa9T1XWndAzM35Bl21pTQJO2K8uFFSGNgo0k7mn
16 | 84qWiKpwTg==
17 | -----END CERTIFICATE REQUEST-----
18 |
--------------------------------------------------------------------------------
/example/nginx/certs/localhost.yarf.nl.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCdXQrZpHUhwA76
3 | o5w/M0UnSZZBQTvzPDGPnVTqrW1Xdsiix4DTa4hgEScvJ78dXPSfMvmoQiYGr7p5
4 | YVU9Cp3r/Vb5SwbmOcBSlXHzqk583XM+rNWosElg9GuWDYXgcsDwh9gX/gtfa6jS
5 | tTGdboZI5IDwuzWyKjOXhCzyIJBFzhHAXj49f9qYSIxvVwqnX7oDoJ8AQ+MEVlvN
6 | gW01uM33Yu2USwFlV9ZoAW/jDVWoNzCb9SpzD4/9f3lNmpfZoNvK6l2ogd8ciSr6
7 | JnSoyydM7/SHvQzUp9kWk4/cmRl9qad1SPl805fedEX9VtjoY0cNiRXa9yXyqN5B
8 | Ijgeg7mTAgMBAAECggEATfZryro8wdTaVvi6D2HCUB2YEIpWPkLDNyi2inITqvKR
9 | onv+6j/rV9UHucgSWmTUWQ8zO1ZSapujYkGDrnNVHDbuYSH3sBZKn1+lDhiBPEGm
10 | uBV+4U09fYh6yOO4QSP5TPGwcOqPDd5TzNiyVRIN+40iCKJnjvZziwyUC/1wHPSd
11 | WZvEV/EIlYk+rKwqowleAOXJ/rl1BsUIc5mikN4QHOS5Wbsr6gZibve9ZQ1WlKei
12 | da0c2qxsq/0nFHrSGk6JyIgKxtnm/F/NrQraHe8fO5dMNT0ZmM3bRiy6TDAcgCkn
13 | 1PdtIAhEgZuYAjIhk8IKLzePvYIxPs8D/F2RgD1VeQKBgQDKfIeiYsNiizDZomEj
14 | ovIdJvWXgU5p4LBWZEYLpzbLlOv6kGVZ5Hn6Z+VRrLYWv9/TScicannJNu/qRGia
15 | 4hlcjUWbkjWLjFt52/kT9VfeONIce04K1/HHBRtPada0hUdzwHCHfNCsT0yB2vxe
16 | HD3079e7Ct0CyoD+hEsMgEYbFwKBgQDG86kVm7qIelMzX3Zwp4ib18H5TsoMMrcY
17 | 22Pf0G6/biaSxixtiRBpKypYuXADf5Q3PJb9maAGBXjd5CGbia+gzegYxP8NYa+C
18 | QwhqCbBBKjRhF3w1JmqVlJyWYamjxIfDdmgTU/IzbUhxOx4MFcAFmnNJ6n3qulbT
19 | 1ELFO3Uy5QKBgBmJNO3EuNFPrnxz3v5IiXIlvKk7tHDj1jk+8hp8HwvznwL9fNqm
20 | Vr++pIv1VQ1va0HRN0yKnQtEM2N+9xY5V2t1oYaqHLiZnduzYykiMs+iqNTQtBnn
21 | ++TWfwg318zyVf2CEm7zzbk9Uu+5d0RDGYkvSiybhR3Z/gSbGH/eGXlHAoGBAK/C
22 | SIjfZ730GxaSakcBqmzLAgEmetal4x1hi+0I7R3OyOL3kf4+jTHrwWBaijt89MqL
23 | i3SEyFspcGrOhGYtD+wGm9luB0iiGPQCFiffYUdgap+vqLonsxdsD53Gr6APGkUy
24 | kKUqjxihndLygAv7FwWcOed98jlw3w4KQeaOLW6FAoGAblQuh68PSYNSEnr5SSF6
25 | crHL8iQx5+k+e+7dp/VpNCUJGxYjRipNLzRstqQgTzhawupUVYpgmymOGt82+skq
26 | WmDHSgPLm5hcjbEOuw1wyDzJaVrR58W6po9Oq4LKUkAq5ST2IGS+ZEtPelUPgBSj
27 | yY3FtxXRPlqi2RyJ5MJ13wA=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/example/nginx/certs/ssl.ext:
--------------------------------------------------------------------------------
1 | authorityKeyIdentifier=keyid,issuer
2 | basicConstraints=CA:FALSE
3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
4 | subjectAltName = @alt_names
5 |
6 | [alt_names]
7 | DNS.1 = identity.localhost.yarf.nl
8 | DNS.2 = resource-provider.localhost.yarf.nl
9 | DNS.3 = resource-provider-api.localhost.yarf.nl
10 |
--------------------------------------------------------------------------------
/example/nginx/conf.d/identity.localhost.yarf.nl.conf:
--------------------------------------------------------------------------------
1 | upstream keycloak {
2 | server keycloak:8080;
3 | }
4 |
5 | server {
6 | listen *:80;
7 | server_name identity.localhost.yarf.nl;
8 | return 301 https://$server_name$request_uri;
9 | }
10 |
11 | server {
12 |
13 | listen *:443;
14 | server_name identity.localhost.yarf.nl;
15 |
16 | # Set up SSL
17 | ssl on;
18 |
19 | ssl_session_cache shared:SSL:1m;
20 | ssl_session_timeout 5m;
21 |
22 | ssl_certificate /etc/nginx/certs/localhost.yarf.nl.cert;
23 | ssl_certificate_key /etc/nginx/certs/localhost.yarf.nl.key;
24 |
25 | # Settings to avoid SSL warnings in browser
26 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don’t use SSLv3 ref: POODLE
27 | ssl_prefer_server_ciphers on;
28 | ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
29 |
30 |
31 | # Logging
32 | access_log /var/log/nginx/identity.localhost.yarf.nl.access.log; # could also put these into 1 file
33 | error_log /var/log/nginx/identity.localhost.yarf.nl.error.log;
34 |
35 |
36 | # Allow empty bodies
37 | client_max_body_size 0;
38 |
39 | # Reverse proxy to Keycloak
40 | location / {
41 |
42 | proxy_pass http://keycloak;
43 |
44 | proxy_buffering off;
45 | proxy_set_header Host $http_host;
46 | proxy_set_header X-Real-IP $remote_addr;
47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
48 | proxy_set_header X-Forwarded-Proto $scheme;
49 |
50 | proxy_cookie_domain identity.localhost.yarf.nl $host;
51 | proxy_pass_header X-XSRF-TOKEN;
52 |
53 | # untested, but taken from https://gist.github.com/nikmartin/5902176#file-nginx-ssl-conf-L25
54 | # and seems useful
55 | proxy_set_header X-NginX-Proxy true;
56 | proxy_read_timeout 5m;
57 | proxy_connect_timeout 5m;
58 |
59 | proxy_redirect off;
60 | }
61 |
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/example/nginx/conf.d/resource-provider-api.localhost.yarf.nl.conf:
--------------------------------------------------------------------------------
1 | upstream resource-provider-api {
2 | server resource-provider-api:8002;
3 | }
4 |
5 | server {
6 | listen *:80;
7 | server_name resource-provider-api.localhost.yarf.nl;
8 | return 301 https://$server_name$request_uri;
9 | }
10 |
11 | server {
12 |
13 | listen 443;
14 | server_name resource-provider-api.localhost.yarf.nl;
15 |
16 | # Set up SSL
17 | ssl on;
18 |
19 | ssl_session_cache shared:SSL:1m;
20 | ssl_session_timeout 5m;
21 |
22 | ssl_certificate /etc/nginx/certs/localhost.yarf.nl.cert;
23 | ssl_certificate_key /etc/nginx/certs/localhost.yarf.nl.key;
24 |
25 | # Settings to avoid SSL warnings in browser
26 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don’t use SSLv3 ref: POODLE
27 | ssl_prefer_server_ciphers on;
28 | ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
29 |
30 |
31 | # Logging
32 | access_log /var/log/nginx/resource-provider-api.localhost.yarf.nl.access.log; # could also put these into 1 file
33 | error_log /var/log/nginx/resource-provider-api.localhost.yarf.nl.error.log;
34 |
35 |
36 | # Allow empty bodies
37 | client_max_body_size 0;
38 |
39 | # Reverse proxy to Keycloak
40 | location / {
41 |
42 | proxy_pass http://resource-provider-api;
43 |
44 | proxy_buffering off;
45 | proxy_set_header Host $http_host;
46 | proxy_set_header X-Real-IP $remote_addr;
47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
48 | proxy_set_header X-Forwarded-Proto $scheme;
49 |
50 | proxy_cookie_domain resource-provider-api.localhost.yarf.nl $host;
51 | proxy_pass_header X-XSRF-TOKEN;
52 |
53 | # untested, but taken from https://gist.github.com/nikmartin/5902176#file-nginx-ssl-conf-L25
54 | # and seems useful
55 | proxy_set_header X-NginX-Proxy true;
56 | proxy_read_timeout 5m;
57 | proxy_connect_timeout 5m;
58 |
59 | proxy_redirect off;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/example/nginx/conf.d/resource-provider.localhost.yarf.nl.conf:
--------------------------------------------------------------------------------
1 | upstream resource-provider {
2 | server resource-provider:8001;
3 | }
4 |
5 | server {
6 | listen *:80;
7 | server_name resource-provider.localhost.yarf.nl;
8 | return 301 https://$server_name$request_uri;
9 | }
10 |
11 | server {
12 |
13 | listen *:443;
14 | server_name resource-provider.localhost.yarf.nl;
15 |
16 | # Set up SSL
17 | ssl on;
18 |
19 | ssl_session_cache shared:SSL:1m;
20 | ssl_session_timeout 5m;
21 |
22 | ssl_certificate /etc/nginx/certs/localhost.yarf.nl.cert;
23 | ssl_certificate_key /etc/nginx/certs/localhost.yarf.nl.key;
24 |
25 | # Settings to avoid SSL warnings in browser
26 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don’t use SSLv3 ref: POODLE
27 | ssl_prefer_server_ciphers on;
28 | ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
29 |
30 |
31 | # Logging
32 | access_log /var/log/nginx/resource-provider.localhost.yarf.nl.access.log; # could also put these into 1 file
33 | error_log /var/log/nginx/resource-provider.localhost.yarf.nl.error.log;
34 |
35 |
36 | # Allow empty bodies
37 | client_max_body_size 0;
38 |
39 | # Reverse proxy to Keycloak
40 | location / {
41 |
42 | proxy_pass http://resource-provider;
43 |
44 | proxy_buffering off;
45 | proxy_set_header Host $http_host;
46 | proxy_set_header X-Real-IP $remote_addr;
47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
48 | proxy_set_header X-Forwarded-Proto $scheme;
49 |
50 | proxy_cookie_domain resource-provider.localhost.yarf.nl $host;
51 | proxy_pass_header X-XSRF-TOKEN;
52 |
53 | # untested, but taken from https://gist.github.com/nikmartin/5902176#file-nginx-ssl-conf-L25
54 | # and seems useful
55 | proxy_set_header X-NginX-Proxy true;
56 | proxy_read_timeout 5m;
57 | proxy_connect_timeout 5m;
58 |
59 | proxy_redirect off;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/example/resource-provider-api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3-alpine
2 |
3 | RUN apk update \
4 | && apk add git openssl-dev libffi-dev python-dev build-base
5 |
6 | RUN mkdir -p /usr/src/app
7 |
8 | WORKDIR /usr/src/app
9 |
10 | COPY requirements.txt /usr/src/app/
11 |
12 | RUN pip install --no-cache-dir -r requirements.txt
13 |
14 | COPY . /usr/src/app
15 |
16 | EXPOSE 8002
17 |
18 | CMD [ "python", "manage.py", "runserver", "0.0.0.0:8002" ]
19 |
--------------------------------------------------------------------------------
/example/resource-provider-api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider-api/__init__.py
--------------------------------------------------------------------------------
/example/resource-provider-api/docker/entrypoint-dev.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | set -e
3 |
4 | pip install -e ./../django-keycloak/
5 | pip install -e ./../python-keycloak-client/ || true
6 |
7 | if [ -f db.sqlite3 ]; then
8 | echo "Application already initialized."
9 | else
10 | echo "Initializing application"
11 |
12 | # Run migrations
13 | python manage.py migrate
14 |
15 | python manage.py load_dynamic_fixtures myapp
16 | fi
17 |
18 | if grep -q Yarf /usr/local/lib/python3.7/site-packages/certifi/cacert.pem
19 | then
20 | echo "CA already added"
21 | else
22 | echo "Add CA to trusted pool"
23 | echo "\n\n# Yarf" >> /usr/local/lib/python3.7/site-packages/certifi/cacert.pem
24 | cat /usr/src/ca.pem >> /usr/local/lib/python3.7/site-packages/certifi/cacert.pem
25 | fi
26 |
27 | exec "$@"
28 |
--------------------------------------------------------------------------------
/example/resource-provider-api/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", "myapp.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError:
10 | # The above import may fail for some other reason. Ensure that the
11 | # issue is really that Django is missing to avoid masking other
12 | # exceptions on Python 2.
13 | try:
14 | import django #noqa: F401
15 | except ImportError:
16 | raise ImportError(
17 | "Couldn't import Django. Are you sure it's installed and "
18 | "available on your PYTHONPATH environment variable? Did you "
19 | "forget to activate a virtual environment?"
20 | )
21 | raise
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider-api/myapp/__init__.py
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class MyAppConfig(AppConfig):
8 | name = 'myapp'
9 |
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/fixtures/0001_admin_user.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 |
3 | from dynamic_fixtures.fixtures import BaseFixture
4 |
5 |
6 | class Fixture(BaseFixture):
7 |
8 | def load(self):
9 | user_model = get_user_model()
10 |
11 | if not user_model.objects.filter(username='admin').exists():
12 | user_model.objects.create_superuser(username='admin',
13 | password='password',
14 | email='admin@example.com')
15 |
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/fixtures/0002_server.py:
--------------------------------------------------------------------------------
1 | from dynamic_fixtures.fixtures import BaseFixture
2 |
3 | from django_keycloak.models import Server
4 |
5 |
6 | class Fixture(BaseFixture):
7 |
8 | def load(self):
9 | Server.objects.get_or_create(
10 | url='https://identity.localhost.yarf.nl'
11 | )
12 |
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/fixtures/0003_realm.py:
--------------------------------------------------------------------------------
1 | from dynamic_fixtures.fixtures import BaseFixture
2 |
3 | from django_keycloak.models import Realm, Server
4 |
5 |
6 | class Fixture(BaseFixture):
7 |
8 | dependencies = (
9 | ('myapp', '0002_server'),
10 | )
11 |
12 | def load(self):
13 | server = Server.objects.get(url='https://identity.localhost.yarf.nl')
14 |
15 | Realm.objects.get_or_create(
16 | server=server,
17 | name='example',
18 | _certs='{"keys": [{"kid": "jr15h-1NMapltoxqLH8aBTD4-XjyfGD8pqUEtqsUJmU", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "4hGf6zcb6KghN61pUROjPptdivncqkgaDcNwFcubw95Lw1IiTsgo__l1P3720Lhsb4Br0w2XWr44fzR8i1kgvow_s5-CG_F3S7OJ1Abz1Au_zSHg3nLd901lWMVrXvVZR1jFCuvIjr9DQAmyHv1-cL8OuBjGKWX7qi3LXQp2oEVRIUO_pM83vQJf1rZH-YUz6-w_g4XeOF-1GRXtaC2xYHbUCEsH1Lo7J-i3im4SgvER74Zh-cpoF_Q2DjK520VJletJBM4SrUh3DcScGVFmwharPI4wDdRXq_-SqQ52r1-X0k3fmj1gcwhLRH6jW0dTTt-T2ROENfjGTBBd5nBJWQ", "e": "AQAB"}]}',
19 | _well_known_oidc='{"issuer": "https://identity.localhost.yarf.nl/auth/realms/example", "authorization_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/auth", "token_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/token", "token_introspection_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/token/introspect", "userinfo_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/userinfo", "end_session_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/logout", "jwks_uri": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/certs", "check_session_iframe": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/login-status-iframe.html", "grant_types_supported": ["authorization_code", "implicit", "refresh_token", "password", "client_credentials"], "response_types_supported": ["code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"], "subject_types_supported": ["public", "pairwise"], "id_token_signing_alg_values_supported": ["RS256"], "userinfo_signing_alg_values_supported": ["RS256"], "request_object_signing_alg_values_supported": ["none", "RS256"], "response_modes_supported": ["query", "fragment", "form_post"], "registration_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/clients-registrations/openid-connect", "token_endpoint_auth_methods_supported": ["private_key_jwt", "client_secret_basic", "client_secret_post"], "token_endpoint_auth_signing_alg_values_supported": ["RS256"], "claims_supported": ["sub", "iss", "auth_time", "name", "given_name", "family_name", "preferred_username", "email"], "claim_types_supported": ["normal"], "claims_parameter_supported": false, "scopes_supported": ["openid", "offline_access"], "request_parameter_supported": true, "request_uri_parameter_supported": true}'
20 | )
21 |
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/fixtures/0004_client.py:
--------------------------------------------------------------------------------
1 | from dynamic_fixtures.fixtures import BaseFixture
2 |
3 | from django_keycloak.models import Realm, Client
4 |
5 |
6 | class Fixture(BaseFixture):
7 |
8 | dependencies = (
9 | ('myapp', '0003_realm'),
10 | )
11 |
12 | def load(self):
13 | realm = Realm.objects.get(name='example')
14 |
15 | Client.objects.get_or_create(
16 | realm=realm,
17 | client_id='resource-provider-api',
18 | secret='145a828b-bbb1-44b0-81f5-d3d669ab59f7'
19 | )
20 |
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/fixtures/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider-api/myapp/fixtures/__init__.py
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider-api/myapp/migrations/__init__.py
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models
5 |
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for resource_provider project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.11.7.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/1.11/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = '1y=4ruy1(qz70k1l$kwe3o^i4^2a_7p)popv@6$sj%!04t^s9l'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = ['*']
29 |
30 | # Application definition
31 |
32 | INSTALLED_APPS = [
33 | 'django.contrib.admin',
34 | 'django.contrib.auth',
35 | 'django.contrib.contenttypes',
36 | 'django.contrib.sessions',
37 | 'django.contrib.messages',
38 | 'django.contrib.staticfiles',
39 |
40 | 'myapp.apps.MyAppConfig',
41 | 'django_keycloak.apps.KeycloakAppConfig',
42 | 'dynamic_fixtures'
43 | ]
44 |
45 | MIDDLEWARE = [
46 | 'django.middleware.security.SecurityMiddleware',
47 | 'django.contrib.sessions.middleware.SessionMiddleware',
48 | 'django.middleware.common.CommonMiddleware',
49 | 'django.middleware.csrf.CsrfViewMiddleware',
50 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
51 | 'django.contrib.messages.middleware.MessageMiddleware',
52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
53 |
54 | 'django_keycloak.middleware.KeycloakStatelessBearerAuthenticationMiddleware',
55 | # 'django_keycloak.middleware.RemoteUserAuthenticationMiddleware',
56 | ]
57 |
58 | PASSWORD_HASHERS = [
59 | 'django_keycloak.hashers.PBKDF2SHA512PasswordHasher',
60 | ]
61 |
62 | AUTHENTICATION_BACKENDS = [
63 | 'django.contrib.auth.backends.ModelBackend',
64 | 'django_keycloak.auth.backends.KeycloakIDTokenAuthorizationBackend'
65 | ]
66 |
67 | ROOT_URLCONF = 'myapp.urls'
68 |
69 | TEMPLATES = [
70 | {
71 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
72 | 'DIRS': [],
73 | 'APP_DIRS': True,
74 | 'OPTIONS': {
75 | 'context_processors': [
76 | 'django.template.context_processors.debug',
77 | 'django.template.context_processors.request',
78 | 'django.contrib.auth.context_processors.auth',
79 | 'django.contrib.messages.context_processors.messages',
80 | ],
81 | },
82 | },
83 | ]
84 |
85 | WSGI_APPLICATION = 'myapp.wsgi.application'
86 |
87 |
88 | # Database
89 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
90 |
91 | DATABASES = {
92 | 'default': {
93 | 'ENGINE': 'django.db.backends.sqlite3',
94 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
95 | }
96 | }
97 |
98 |
99 | # Password validation
100 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
101 |
102 | AUTH_PASSWORD_VALIDATORS = [
103 | {
104 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
105 | },
106 | {
107 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
108 | },
109 | {
110 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
111 | },
112 | {
113 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
114 | },
115 | ]
116 |
117 | KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.RemoteUserOpenIdConnectProfile'
118 |
119 | # Internationalization
120 | # https://docs.djangoproject.com/en/1.11/topics/i18n/
121 |
122 | LANGUAGE_CODE = 'en-us'
123 |
124 | TIME_ZONE = 'UTC'
125 |
126 | USE_I18N = True
127 |
128 | USE_L10N = True
129 |
130 | USE_TZ = True
131 |
132 |
133 | # Static files (CSS, JavaScript, Images)
134 | # https://docs.djangoproject.com/en/1.11/howto/static-files/
135 |
136 | STATIC_URL = '/static/'
137 |
138 | LOGGING = {
139 | 'version': 1,
140 | 'disable_existing_loggers': False,
141 | 'handlers': {
142 | 'console': {
143 | 'level': 'DEBUG',
144 | 'class': 'logging.StreamHandler',
145 | },
146 | },
147 | 'loggers': {
148 | '': {
149 | 'handlers': ['console'],
150 | 'level': 'DEBUG',
151 | },
152 | }
153 | }
154 |
155 | KEYCLOAK_BEARER_AUTHENTICATION_EXEMPT_PATHS = [
156 | r'^admin/',
157 | ]
158 |
159 | KEYCLOAK_SKIP_SSL_VERIFY = True
160 |
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/urls.py:
--------------------------------------------------------------------------------
1 | """resource_provider URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15 | """
16 | from django.conf.urls import url, include
17 | from django.contrib import admin
18 |
19 | from myapp import views
20 |
21 |
22 | urlpatterns = [
23 | url(r'^api/end-point$', views.api_end_point),
24 | url(r'^api/authenticated-end-point$', views.authenticated_end_point),
25 | url(r'^admin/', admin.site.urls),
26 | ]
27 |
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import logging
5 |
6 | from django.http.response import JsonResponse
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | def api_end_point(request):
12 | return JsonResponse({'foo': 'bar'})
13 |
14 |
15 | def authenticated_end_point(request):
16 |
17 | return JsonResponse({
18 | 'user': {
19 | 'first_name': request.user.first_name,
20 | 'last_name': request.user.last_name
21 | }
22 | })
--------------------------------------------------------------------------------
/example/resource-provider-api/myapp/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for resource_provider project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/example/resource-provider-api/requirements.txt:
--------------------------------------------------------------------------------
1 | django>=2.0.10
2 | git+git://github.com/Peter-Slump/django-keycloak.git#egg=django-keycloak
3 | django-dynamic-fixtures==0.1.7
--------------------------------------------------------------------------------
/example/resource-provider/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3-alpine
2 |
3 | RUN apk update \
4 | && apk add git openssl-dev libffi-dev python-dev build-base
5 |
6 | RUN mkdir -p /usr/src/app
7 |
8 | WORKDIR /usr/src/app
9 |
10 | COPY requirements.txt /usr/src/app/
11 |
12 | RUN pip install --no-cache-dir -r requirements.txt
13 |
14 | COPY . /usr/src/app
15 |
16 | EXPOSE 8001
17 |
18 | CMD [ "python", "manage.py", "runserver", "0.0.0.0:8001" ]
19 |
--------------------------------------------------------------------------------
/example/resource-provider/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider/__init__.py
--------------------------------------------------------------------------------
/example/resource-provider/docker/entrypoint-dev.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | set -e
3 |
4 | pip install -e ./../django-keycloak/
5 | pip install -e ./../python-keycloak-client/ || true
6 | pip install -e ./../django-dynamic-fixtures/ || true
7 |
8 | if [ -f db.sqlite3 ]; then
9 | echo "Application already initialized."
10 | else
11 | echo "Initializing application"
12 |
13 | # Run migrations
14 | python manage.py migrate
15 |
16 | python manage.py load_dynamic_fixtures myapp
17 | fi
18 |
19 | if grep -q Yarf /usr/local/lib/python3.7/site-packages/certifi/cacert.pem
20 | then
21 | echo "CA already added"
22 | else
23 | echo "Add CA to trusted pool"
24 | echo "\n\n# Yarf" >> /usr/local/lib/python3.7/site-packages/certifi/cacert.pem
25 | cat /usr/src/ca.pem >> /usr/local/lib/python3.7/site-packages/certifi/cacert.pem
26 | fi
27 |
28 | exec "$@"
29 |
--------------------------------------------------------------------------------
/example/resource-provider/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", "myapp.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError:
10 | # The above import may fail for some other reason. Ensure that the
11 | # issue is really that Django is missing to avoid masking other
12 | # exceptions on Python 2.
13 | try:
14 | import django #noqa: F401
15 | except ImportError:
16 | raise ImportError(
17 | "Couldn't import Django. Are you sure it's installed and "
18 | "available on your PYTHONPATH environment variable? Did you "
19 | "forget to activate a virtual environment?"
20 | )
21 | raise
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider/myapp/__init__.py
--------------------------------------------------------------------------------
/example/resource-provider/myapp/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class MyAppConfig(AppConfig):
8 | name = 'myapp'
9 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/fixtures/0001_admin_user.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 |
3 | from dynamic_fixtures.fixtures import BaseFixture
4 |
5 |
6 | class Fixture(BaseFixture):
7 |
8 | def load(self):
9 | user_model = get_user_model()
10 |
11 | if not user_model.objects.filter(username='admin').exists():
12 | user_model.objects.create_superuser(username='admin',
13 | password='password',
14 | email='admin@example.com')
15 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/fixtures/0002_server.py:
--------------------------------------------------------------------------------
1 | from dynamic_fixtures.fixtures import BaseFixture
2 |
3 | from django_keycloak.models import Server
4 |
5 |
6 | class Fixture(BaseFixture):
7 |
8 | def load(self):
9 | Server.objects.get_or_create(
10 | url='https://identity.localhost.yarf.nl'
11 | )
12 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/fixtures/0003_realm.py:
--------------------------------------------------------------------------------
1 | from dynamic_fixtures.fixtures import BaseFixture
2 |
3 | from django_keycloak.models import Realm, Server, RemoteClient
4 |
5 |
6 | class Fixture(BaseFixture):
7 |
8 | dependencies = (
9 | ('myapp', '0002_server'),
10 | )
11 |
12 | def load(self):
13 | server = Server.objects.get(url='https://identity.localhost.yarf.nl')
14 |
15 | realm, created = Realm.objects.get_or_create(
16 | server=server,
17 | name='example',
18 | _certs='{"keys": [{"kid": "jr15h-1NMapltoxqLH8aBTD4-XjyfGD8pqUEtqsUJmU", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "4hGf6zcb6KghN61pUROjPptdivncqkgaDcNwFcubw95Lw1IiTsgo__l1P3720Lhsb4Br0w2XWr44fzR8i1kgvow_s5-CG_F3S7OJ1Abz1Au_zSHg3nLd901lWMVrXvVZR1jFCuvIjr9DQAmyHv1-cL8OuBjGKWX7qi3LXQp2oEVRIUO_pM83vQJf1rZH-YUz6-w_g4XeOF-1GRXtaC2xYHbUCEsH1Lo7J-i3im4SgvER74Zh-cpoF_Q2DjK520VJletJBM4SrUh3DcScGVFmwharPI4wDdRXq_-SqQ52r1-X0k3fmj1gcwhLRH6jW0dTTt-T2ROENfjGTBBd5nBJWQ", "e": "AQAB"}]}',
19 | _well_known_oidc='{"issuer": "https://identity.localhost.yarf.nl/auth/realms/example", "authorization_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/auth", "token_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/token", "token_introspection_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/token/introspect", "userinfo_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/userinfo", "end_session_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/logout", "jwks_uri": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/certs", "check_session_iframe": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/login-status-iframe.html", "grant_types_supported": ["authorization_code", "implicit", "refresh_token", "password", "client_credentials"], "response_types_supported": ["code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"], "subject_types_supported": ["public", "pairwise"], "id_token_signing_alg_values_supported": ["RS256"], "userinfo_signing_alg_values_supported": ["RS256"], "request_object_signing_alg_values_supported": ["none", "RS256"], "response_modes_supported": ["query", "fragment", "form_post"], "registration_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/clients-registrations/openid-connect", "token_endpoint_auth_methods_supported": ["private_key_jwt", "client_secret_basic", "client_secret_post"], "token_endpoint_auth_signing_alg_values_supported": ["RS256"], "claims_supported": ["sub", "iss", "auth_time", "name", "given_name", "family_name", "preferred_username", "email"], "claim_types_supported": ["normal"], "claims_parameter_supported": false, "scopes_supported": ["openid", "offline_access"], "request_parameter_supported": true, "request_uri_parameter_supported": true}'
20 | )
21 |
22 | RemoteClient.objects.get_or_create(
23 | name='resource-provider-api',
24 | realm=realm
25 | )
26 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/fixtures/0004_client.py:
--------------------------------------------------------------------------------
1 | from dynamic_fixtures.fixtures import BaseFixture
2 |
3 | from django_keycloak.models import Realm, Client
4 |
5 |
6 | class Fixture(BaseFixture):
7 |
8 | dependencies = (
9 | ('myapp', '0003_realm'),
10 | )
11 |
12 | def load(self):
13 | realm = Realm.objects.get(name='example')
14 |
15 | Client.objects.get_or_create(
16 | realm=realm,
17 | client_id='resource-provider',
18 | secret='f40347aa-728d-4599-aba9-26f4a69d6f1e'
19 | )
20 |
21 |
22 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/fixtures/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider/myapp/fixtures/__init__.py
--------------------------------------------------------------------------------
/example/resource-provider/myapp/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.3 on 2018-03-20 21:28
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ('django_keycloak', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='ExchangedToken',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('access_token', models.TextField(null=True)),
21 | ('expires_before', models.DateTimeField(null=True)),
22 | ('refresh_token', models.TextField(null=True)),
23 | ('refresh_expires_before', models.DateTimeField(null=True)),
24 | ('oidc_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_keycloak.OpenIdConnectProfile')),
25 | ],
26 | ),
27 | migrations.CreateModel(
28 | name='RemoteClient',
29 | fields=[
30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
31 | ('name', models.CharField(max_length=255)),
32 | ('realm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='remote_clients', to='django_keycloak.Realm')),
33 | ],
34 | ),
35 | migrations.AddField(
36 | model_name='exchangedtoken',
37 | name='remote_client',
38 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exchanged_tokens', to='myapp.RemoteClient'),
39 | ),
40 | migrations.AlterUniqueTogether(
41 | name='exchangedtoken',
42 | unique_together={('oidc_profile', 'remote_client')},
43 | ),
44 | ]
45 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/migrations/0002_auto_20180322_2059.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.3 on 2018-03-22 20:59
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('myapp', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterUniqueTogether(
14 | name='exchangedtoken',
15 | unique_together=set(),
16 | ),
17 | migrations.RemoveField(
18 | model_name='exchangedtoken',
19 | name='oidc_profile',
20 | ),
21 | migrations.RemoveField(
22 | model_name='exchangedtoken',
23 | name='remote_client',
24 | ),
25 | migrations.RemoveField(
26 | model_name='remoteclient',
27 | name='realm',
28 | ),
29 | migrations.DeleteModel(
30 | name='ExchangedToken',
31 | ),
32 | migrations.DeleteModel(
33 | name='RemoteClient',
34 | ),
35 | ]
36 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider/myapp/migrations/__init__.py
--------------------------------------------------------------------------------
/example/resource-provider/myapp/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models
5 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for resource_provider project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.11.7.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/1.11/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = '1y=4ruy1(qz70k1l$kwe3o^i4^2a_7p)popv@6$sj%!04t^s9l'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = ['*']
29 |
30 | LOGIN_URL = 'keycloak_login'
31 | LOGOUT_REDIRECT_URL = 'keycloak_login'
32 |
33 | SECURE_SSL_REDIRECT = True
34 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
35 |
36 | # Application definition
37 |
38 | INSTALLED_APPS = [
39 | 'django.contrib.admin',
40 | 'django.contrib.auth',
41 | 'django.contrib.contenttypes',
42 | 'django.contrib.sessions',
43 | 'django.contrib.messages',
44 | 'django.contrib.staticfiles',
45 |
46 | 'myapp.apps.MyAppConfig',
47 | 'django_keycloak.apps.KeycloakAppConfig',
48 | 'dynamic_fixtures'
49 | ]
50 |
51 | MIDDLEWARE = [
52 | 'django.middleware.security.SecurityMiddleware',
53 | 'django.contrib.sessions.middleware.SessionMiddleware',
54 | 'django.middleware.common.CommonMiddleware',
55 | 'django.middleware.csrf.CsrfViewMiddleware',
56 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
57 | 'django.contrib.messages.middleware.MessageMiddleware',
58 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
59 |
60 | 'django_keycloak.middleware.BaseKeycloakMiddleware',
61 | 'django_keycloak.middleware.RemoteUserAuthenticationMiddleware',
62 | ]
63 |
64 | PASSWORD_HASHERS = [
65 | 'django_keycloak.hashers.PBKDF2SHA512PasswordHasher',
66 | ]
67 |
68 | AUTHENTICATION_BACKENDS = [
69 | 'django.contrib.auth.backends.ModelBackend',
70 | 'django_keycloak.auth.backends.KeycloakAuthorizationCodeBackend',
71 | ]
72 |
73 | ROOT_URLCONF = 'myapp.urls'
74 |
75 | TEMPLATES = [
76 | {
77 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
78 | 'DIRS': [],
79 | 'APP_DIRS': True,
80 | 'OPTIONS': {
81 | 'context_processors': [
82 | 'django.template.context_processors.debug',
83 | 'django.template.context_processors.request',
84 | 'django.contrib.auth.context_processors.auth',
85 | 'django.contrib.messages.context_processors.messages',
86 | ],
87 | },
88 | },
89 | ]
90 |
91 | WSGI_APPLICATION = 'myapp.wsgi.application'
92 |
93 |
94 | # Database
95 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
96 |
97 | DATABASES = {
98 | 'default': {
99 | 'ENGINE': 'django.db.backends.sqlite3',
100 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
101 | }
102 | }
103 |
104 |
105 | # Password validation
106 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
107 |
108 | AUTH_PASSWORD_VALIDATORS = [
109 | {
110 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
111 | },
112 | {
113 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
114 | },
115 | {
116 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
117 | },
118 | {
119 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
120 | },
121 | ]
122 |
123 | # KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.RemoteUserOpenIdConnectProfile'
124 | # KEYCLOAK_PERMISSIONS_METHOD = 'resource'
125 |
126 | CONTEXT_PROCESSORS = [
127 |
128 | ]
129 |
130 | # Internationalization
131 | # https://docs.djangoproject.com/en/1.11/topics/i18n/
132 |
133 | LANGUAGE_CODE = 'en-us'
134 |
135 | TIME_ZONE = 'UTC'
136 |
137 | USE_I18N = True
138 |
139 | USE_L10N = True
140 |
141 | USE_TZ = True
142 |
143 |
144 | # Static files (CSS, JavaScript, Images)
145 | # https://docs.djangoproject.com/en/1.11/howto/static-files/
146 |
147 | STATIC_URL = '/static/'
148 |
149 | LOGGING = {
150 | 'version': 1,
151 | 'disable_existing_loggers': False,
152 | 'handlers': {
153 | 'console': {
154 | 'level': 'DEBUG',
155 | 'class': 'logging.StreamHandler',
156 | },
157 | },
158 | 'loggers': {
159 | '': {
160 | 'handlers': ['console'],
161 | 'level': 'DEBUG',
162 | },
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/templates/403.html:
--------------------------------------------------------------------------------
1 | {% extends 'myapp/base.html' %}
2 |
3 | {% block main %}
4 | Forbidden
5 | {% endblock %}
--------------------------------------------------------------------------------
/example/resource-provider/myapp/templates/myapp/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 | Keycloak authentication
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 | {% if user.is_authenticated %}
23 |
24 | Signed in as {{ user.get_full_name }}
25 | {% if user.oidc_profile %}| Manage Profile {% endif %}
26 | | Logout
27 |
28 | {% else %}
29 |
Sign in
30 | {% endif%}
31 |
32 |
33 |
34 |
35 |
36 | {% block main %}{% endblock %}
37 |
38 |
39 | {% include 'django_keycloak/includes/session_iframe_support.html' %}
40 |
41 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/templates/myapp/home.html:
--------------------------------------------------------------------------------
1 | {% extends 'myapp/base.html' %}
2 |
3 | {% block main %}
4 |
19 | {% if not user.is_authenticated %}
20 | Login with username: testuser
and password: password
.
21 | {% endif %}
22 | {% endblock %}
--------------------------------------------------------------------------------
/example/resource-provider/myapp/templates/myapp/permission.html:
--------------------------------------------------------------------------------
1 | {% extends 'myapp/base.html' %}
2 |
3 | {% block main %}
4 | This page is only accessible when the user has the permission named: "some-permission"
5 | {% endblock %}
--------------------------------------------------------------------------------
/example/resource-provider/myapp/templates/myapp/secured.html:
--------------------------------------------------------------------------------
1 | {% extends 'myapp/base.html' %}
2 |
3 | {% block main %}
4 | Secured area
5 |
6 | Available permissions:
7 |
8 | {% for perm in permissions %}
9 | {{ perm }}
10 | {% endfor%}
11 |
12 |
13 | JWT:
14 |
15 | Below you can find the contents of the access_token
JWT (JSON Web Token).
16 | You can also inspect the token on jwt.io
17 |
18 | {{ jwt }}
19 |
20 | API call result:
21 |
22 | The application made a request to the resource-provider-api . The local access token is exchanged for a token which is valid for the client configured in this application. Please read more about token exchange in the Keycloak documentation .
23 |
24 | {{ api_result }}
25 |
26 | {% endblock %}
--------------------------------------------------------------------------------
/example/resource-provider/myapp/urls.py:
--------------------------------------------------------------------------------
1 | """resource_provider URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15 | """
16 | from django.conf.urls import url, include
17 | from django.contrib import admin
18 |
19 | from myapp import views
20 |
21 |
22 | urlpatterns = [
23 | url(r'^$', views.Home.as_view(), name='index'),
24 | url(r'^secured$', views.Secured.as_view(), name='secured'),
25 | url(r'^permission$', views.Permission.as_view(), name='permission'),
26 | url(r'^keycloak/', include('django_keycloak.urls')),
27 | url(r'^admin/', admin.site.urls),
28 | ]
29 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import json
5 | import logging
6 | import requests
7 |
8 | from django.contrib.auth.mixins import (
9 | LoginRequiredMixin,
10 | PermissionRequiredMixin
11 | )
12 | from django.views.generic.base import TemplateView
13 | from jose.exceptions import JWTError
14 |
15 | import django_keycloak.services.oidc_profile
16 | import django_keycloak.services.remote_client
17 |
18 |
19 | logger = logging.getLogger(__name__)
20 |
21 |
22 | class Home(TemplateView):
23 | template_name = 'myapp/home.html'
24 |
25 |
26 | class Secured(LoginRequiredMixin, TemplateView):
27 | template_name = 'myapp/secured.html'
28 |
29 | def call_api(self):
30 | if not hasattr(self.request.user, 'oidc_profile'):
31 | return None
32 | oidc_profile = self.request.user.oidc_profile
33 | remote_client = oidc_profile.realm.remote_clients.get(
34 | name='resource-provider-api')
35 |
36 | access_token = django_keycloak.services.remote_client.get_active_remote_client_token(
37 | oidc_profile=oidc_profile, remote_client=remote_client)
38 |
39 | result = requests.get(
40 | 'https://resource-provider-api.localhost.yarf.nl/api/authenticated-end-point',
41 | verify=False,
42 | headers={
43 | 'Authorization': 'Bearer {}'.format(access_token)
44 | }
45 | )
46 | return result.json()
47 |
48 | def get_context_data(self, **kwargs):
49 | api_result = self.call_api()
50 | try:
51 | jwt = self.get_decoded_jwt()
52 | except JWTError:
53 | jwt = None
54 |
55 | return super(Secured, self).get_context_data(
56 | permissions=self.request.user.get_all_permissions(),
57 | access_token=self.request.user.oidc_profile.access_token,
58 | api_result=api_result,
59 | jwt=json.dumps(jwt, sort_keys=True, indent=4, separators=(',', ': ')) if jwt else None,
60 | op_location=self.request.realm.well_known_oidc['check_session_iframe'],
61 | **kwargs
62 | )
63 |
64 | def get_decoded_jwt(self):
65 | if not hasattr(self.request.user, 'oidc_profile'):
66 | return None
67 |
68 | oidc_profile = self.request.user.oidc_profile
69 | client = oidc_profile.realm.client
70 |
71 | return client.openid_api_client.decode_token(
72 | token=oidc_profile.access_token,
73 | key=client.realm.certs,
74 | algorithms=client.openid_api_client.well_known[
75 | 'id_token_signing_alg_values_supported']
76 | )
77 |
78 |
79 | class Permission(PermissionRequiredMixin, TemplateView):
80 | raise_exception = True
81 | template_name = 'myapp/permission.html'
82 | permission_required = 'some-permission'
83 |
--------------------------------------------------------------------------------
/example/resource-provider/myapp/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for resource_provider project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/example/resource-provider/requirements.txt:
--------------------------------------------------------------------------------
1 | django>=2.0.10
2 | git+git://github.com/Peter-Slump/django-keycloak.git#egg=django-keycloak
3 | django-dynamic-fixtures==0.1.7
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | DJANGO_SETTINGS_MODULE = django_keycloak.tests.settings
3 | python_files = tests.py test_*.py *_tests.py
4 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 0.1.2-dev
3 | commit = True
4 | tag = True
5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))?
6 | serialize =
7 | {major}.{minor}.{patch}-{release}
8 | {major}.{minor}.{patch}
9 |
10 | [bumpversion:file:setup.py]
11 |
12 | [bumpversion:file:docs/conf.py]
13 |
14 | [bumpversion:file:sonar-project.properties]
15 |
16 | [bumpversion:file:README.rst]
17 | search = **unreleased**
18 | replace = **unreleased**
19 | **v{new_version}**
20 |
21 | [bumpversion:part:release]
22 | optional_value = gamma
23 | values =
24 | dev
25 | gamma
26 |
27 | [aliases]
28 | test = pytest
29 |
30 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from setuptools import setup, find_packages
4 |
5 | VERSION = '0.1.2-dev'
6 |
7 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
8 | README = readme.read()
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-keycloak',
15 | version=VERSION,
16 | long_description=README,
17 | package_dir={'': 'src'},
18 | packages=find_packages('src'),
19 | extras_require={
20 | 'dev': [
21 | 'bumpversion==0.5.3',
22 | 'twine',
23 | ],
24 | 'doc': [
25 | 'Sphinx==1.4.4',
26 | 'sphinx-autobuild==0.6.0',
27 | ]
28 | },
29 | setup_requires=[
30 | 'pytest-runner',
31 | 'python-keycloak-client',
32 | ],
33 | install_requires=[
34 | 'python-keycloak-client>=0.2.2',
35 | 'Django>=1.11',
36 | ],
37 | tests_require=[
38 | 'pytest-django',
39 | 'pytest-cov',
40 | 'mock>=2.0',
41 | 'factory-boy',
42 | 'freezegun'
43 | ],
44 | url='https://github.com/Peter-Slump/django-keycloak',
45 | license='MIT',
46 | author='Peter Slump',
47 | author_email='peter@yarf.nl',
48 | description='Install Django Keycloak.',
49 | classifiers=[]
50 |
51 | )
52 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=Peter-Slump_django-keycloak
2 | sonar.organization=peter-slump-github
3 | sonar.projectName=Django Keycloak
4 | sonar.projectVersion=0.1.2-dev
5 |
6 | # =====================================================
7 | # Meta-data for the project
8 | # =====================================================
9 |
10 | sonar.links.homepage=https://github.com/Peter-Slump/django-keycloak
11 | sonar.links.ci=https://github.com/Peter-Slump/django-keycloak
12 | sonar.links.scm=https://github.com/Peter-Slump/django-keycloak
13 |
14 | # =====================================================
15 | # Properties that will be shared amongst all modules
16 | # =====================================================
17 |
18 | # SQ standard properties
19 | sonar.sources=src
20 | sonar.exclusions=/src/tests/**/*.py
21 | sonar.python.coverage.reportPath=coverage.xml
--------------------------------------------------------------------------------
/src/django_keycloak/__init__.py:
--------------------------------------------------------------------------------
1 | from . import app_settings as defaults
2 | from django.conf import settings
3 |
4 |
5 | default_app_config = 'django_keycloak.apps.KeycloakAppConfig'
6 |
7 | # Set some app default settings
8 | for name in dir(defaults):
9 | if name.isupper() and not hasattr(settings, name):
10 | setattr(settings, name, getattr(defaults, name))
11 |
--------------------------------------------------------------------------------
/src/django_keycloak/admin/__init__.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from django_keycloak.admin.realm import RealmAdmin
4 | from django_keycloak.admin.server import ServerAdmin
5 | from django_keycloak.models import Server, Realm
6 |
7 | admin.site.register(Realm, RealmAdmin)
8 | admin.site.register(Server, ServerAdmin)
9 |
--------------------------------------------------------------------------------
/src/django_keycloak/admin/realm.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin, messages
2 | from keycloak.exceptions import KeycloakClientError
3 | from requests.exceptions import HTTPError
4 |
5 | from django_keycloak.models import (
6 | Client,
7 | OpenIdConnectProfile,
8 | RemoteClient
9 | )
10 | import django_keycloak.services.permissions
11 | import django_keycloak.services.realm
12 | import django_keycloak.services.uma
13 |
14 |
15 | def refresh_open_id_connect_well_known(modeladmin, request, queryset):
16 | for realm in queryset:
17 | django_keycloak.services.realm.refresh_well_known_oidc(realm=realm)
18 | modeladmin.message_user(
19 | request=request,
20 | message='OpenID Connect .well-known refreshed',
21 | level=messages.SUCCESS
22 | )
23 |
24 |
25 | refresh_open_id_connect_well_known.short_description = 'Refresh OpenID ' \
26 | 'Connect .well-known'
27 |
28 |
29 | def refresh_certs(modeladmin, request, queryset):
30 | for realm in queryset:
31 | django_keycloak.services.realm.refresh_certs(realm=realm)
32 | modeladmin.message_user(
33 | request=request,
34 | message='Certificates refreshed',
35 | level=messages.SUCCESS
36 | )
37 |
38 |
39 | refresh_certs.short_description = 'Refresh Certificates'
40 |
41 |
42 | def clear_client_tokens(modeladmin, request, queryset):
43 | OpenIdConnectProfile.objects.filter(realm__in=queryset).update(
44 | access_token=None,
45 | expires_before=None,
46 | refresh_token=None,
47 | refresh_expires_before=None
48 | )
49 | modeladmin.message_user(
50 | request=request,
51 | message='Tokens cleared',
52 | level=messages.SUCCESS
53 | )
54 |
55 |
56 | clear_client_tokens.short_description = 'Clear client tokens'
57 |
58 |
59 | def synchronize_permissions(modeladmin, request, queryset):
60 | for realm in queryset:
61 | try:
62 | django_keycloak.services.permissions.synchronize(
63 | client=realm.client)
64 | except HTTPError as e:
65 | if e.response.status_code == 403:
66 | modeladmin.message_user(
67 | request=request,
68 | message='Forbidden for {}. Does the client\'s service '
69 | 'account has the "keycloak_client" role?'.format(
70 | realm.name
71 | ),
72 | level=messages.ERROR
73 | )
74 | return
75 | else:
76 | raise
77 | modeladmin.message_user(
78 | request=request,
79 | message='Permissions synchronized',
80 | level=messages.SUCCESS
81 | )
82 |
83 |
84 | synchronize_permissions.short_description = 'Synchronize permissions'
85 |
86 |
87 | def synchronize_resources(modeladmin, request, queryset):
88 | for realm in queryset:
89 | try:
90 | django_keycloak.services.uma.synchronize_client(
91 | client=realm.client)
92 | except KeycloakClientError as e:
93 | if e.original_exc.response.status_code == 400:
94 | modeladmin.message_user(
95 | request=request,
96 | message='Forbidden for {}. Is "Remote Resource '
97 | 'Management" enabled for the related client?'
98 | .format(
99 | realm.name
100 | ),
101 | level=messages.ERROR
102 | )
103 | return
104 | else:
105 | raise
106 | modeladmin.message_user(
107 | request=request,
108 | message='Resources synchronized',
109 | level=messages.SUCCESS
110 | )
111 |
112 |
113 | synchronize_resources.short_description = 'Synchronize models as Keycloak ' \
114 | 'resources'
115 |
116 |
117 | class ClientAdmin(admin.TabularInline):
118 |
119 | model = Client
120 |
121 | fields = ('client_id', 'secret')
122 |
123 |
124 | class RemoteClientAdmin(admin.TabularInline):
125 |
126 | model = RemoteClient
127 |
128 | extra = 1
129 |
130 | fields = ('name',)
131 |
132 |
133 | class RealmAdmin(admin.ModelAdmin):
134 |
135 | inlines = [ClientAdmin, RemoteClientAdmin]
136 |
137 | actions = [
138 | refresh_open_id_connect_well_known,
139 | refresh_certs,
140 | clear_client_tokens,
141 | synchronize_permissions,
142 | synchronize_resources
143 | ]
144 |
145 | fieldsets = (
146 | (None, {
147 | 'fields': ('name',)
148 | }),
149 | ('Location', {
150 | 'fields': ('server', '_well_known_oidc',)
151 | })
152 |
153 | )
154 |
155 | readonly_fields = ('_well_known_oidc',)
156 |
--------------------------------------------------------------------------------
/src/django_keycloak/admin/server.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 |
4 | class ServerAdmin(admin.ModelAdmin):
5 |
6 | fieldsets = (
7 | ('Location', {
8 | 'fields': ('url', 'internal_url')
9 | }),
10 | )
11 |
--------------------------------------------------------------------------------
/src/django_keycloak/app_settings.py:
--------------------------------------------------------------------------------
1 | # Configure the model which need to be used to store the Open ID connect
2 | # profile. There are two choices:
3 | # - django_keycloak.OpenIdConnectProfile (Default) a local User object get
4 | # created for the logged in identity.
5 | # - django_keycloak.RemoteUserOpenIdConnectProfile with this model there will
6 | # be no local user stored for the logged in identity.
7 | KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.OpenIdConnectProfile'
8 |
9 | # Class which will be used as User object in case of the remote user OIDC
10 | # Profile
11 | KEYCLOAK_REMOTE_USER_MODEL = 'django_keycloak.remote_user.KeycloakRemoteUser'
12 | KEYCLOAK_PERMISSIONS_METHOD = 'role' # 'role' of 'resource'
13 |
--------------------------------------------------------------------------------
/src/django_keycloak/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps.config import AppConfig
2 |
3 |
4 | class KeycloakAppConfig(AppConfig):
5 | name = 'django_keycloak'
6 | verbose_name = 'Keycloak'
7 |
--------------------------------------------------------------------------------
/src/django_keycloak/auth/__init__.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import HASH_SESSION_KEY, BACKEND_SESSION_KEY, \
2 | _get_backends
3 | from django.contrib.auth.models import AnonymousUser
4 | from django.middleware.csrf import rotate_token
5 | from django.utils import timezone
6 |
7 | import django_keycloak.services.oidc_profile
8 |
9 |
10 | # Using a different session key than the standard django.contrib.auth to
11 | # make sure there is no cross-referencing between UserModel and RemoteUserModel
12 | REMOTE_SESSION_KEY = '_auth_remote_user_id'
13 |
14 |
15 | def _get_user_session_key(request):
16 | return str(request.session[REMOTE_SESSION_KEY])
17 |
18 |
19 | def get_remote_user(request):
20 | """
21 |
22 | :param request:
23 | :return:
24 | """
25 | sub = request.session.get(REMOTE_SESSION_KEY)
26 |
27 | user = None
28 |
29 | OpenIdConnectProfile = django_keycloak.services.oidc_profile\
30 | .get_openid_connect_profile_model()
31 |
32 | try:
33 | oidc_profile = OpenIdConnectProfile.objects.get(
34 | realm=request.realm, sub=sub)
35 | except OpenIdConnectProfile.DoesNotExist:
36 | pass
37 | else:
38 | if oidc_profile.refresh_expires_before > timezone.now():
39 | user = oidc_profile.user
40 |
41 | return user or AnonymousUser()
42 |
43 |
44 | def remote_user_login(request, user, backend=None):
45 | """
46 | Creates a session for the user.
47 | Based on the login function django.contrib.auth.login but uses a slightly
48 | different approach since the user is not backed by a database model.
49 | :param request:
50 | :param user:
51 | :param backend:
52 | :return:
53 | """
54 | session_auth_hash = ''
55 | if user is None:
56 | user = request.user
57 |
58 | if REMOTE_SESSION_KEY in request.session:
59 | if _get_user_session_key(request) != user.identifier:
60 | request.session.flush()
61 | else:
62 | request.session.cycle_key()
63 |
64 | try:
65 | backend = backend or user.backend
66 | except AttributeError:
67 | backends = _get_backends(return_tuples=True)
68 | if len(backends) == 1:
69 | _, backend = backends[0]
70 | else:
71 | raise ValueError(
72 | 'You have multiple authentication backends configured and '
73 | 'therefore must provide the `backend` argument or set the '
74 | '`backend` attribute on the user.'
75 | )
76 |
77 | if not hasattr(user, 'identifier'):
78 | raise ValueError(
79 | 'The user does not have an identifier or the identifier is empty.'
80 | )
81 |
82 | request.session[REMOTE_SESSION_KEY] = user.identifier
83 | request.session[BACKEND_SESSION_KEY] = backend
84 | request.session[HASH_SESSION_KEY] = session_auth_hash
85 | if hasattr(request, 'user'):
86 | request.user = user
87 | rotate_token(request)
88 |
--------------------------------------------------------------------------------
/src/django_keycloak/auth/backends.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.conf import settings
4 | from django.contrib.auth import get_user_model
5 | from django.core.exceptions import ImproperlyConfigured
6 | from django.utils import timezone
7 | from jose.exceptions import (
8 | ExpiredSignatureError,
9 | JWTClaimsError,
10 | JWTError,
11 | )
12 | from keycloak.exceptions import KeycloakClientError
13 |
14 | import django_keycloak.services.oidc_profile
15 |
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | class KeycloakAuthorizationBase(object):
21 |
22 | def get_user(self, user_id):
23 | UserModel = get_user_model()
24 |
25 | try:
26 | user = UserModel.objects.select_related('oidc_profile__realm').get(
27 | pk=user_id)
28 | except UserModel.DoesNotExist:
29 | return None
30 |
31 | if user.oidc_profile.refresh_expires_before > timezone.now():
32 | return user
33 |
34 | return None
35 |
36 | def get_all_permissions(self, user_obj, obj=None):
37 | if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
38 | return set()
39 | if not hasattr(user_obj, '_keycloak_perm_cache'):
40 | user_obj._keycloak_perm_cache = self.get_keycloak_permissions(
41 | user_obj=user_obj)
42 | return user_obj._keycloak_perm_cache
43 |
44 | def get_keycloak_permissions(self, user_obj):
45 | if not hasattr(user_obj, 'oidc_profile'):
46 | return set()
47 |
48 | rpt_decoded = django_keycloak.services.oidc_profile\
49 | .get_entitlement(oidc_profile=user_obj.oidc_profile)
50 |
51 | if settings.KEYCLOAK_PERMISSIONS_METHOD == 'role':
52 | return [
53 | role for role in rpt_decoded['resource_access'].get(
54 | user_obj.oidc_profile.realm.client.client_id,
55 | {'roles': []}
56 | )['roles']
57 | ]
58 | elif settings.KEYCLOAK_PERMISSIONS_METHOD == 'resource':
59 | permissions = []
60 | for p in rpt_decoded['authorization'].get('permissions', []):
61 | if 'scopes' in p:
62 | for scope in p['scopes']:
63 | if '.' in p['resource_set_name']:
64 | app, model = p['resource_set_name'].split('.', 1)
65 | permissions.append('{app}.{scope}_{model}'.format(
66 | app=app,
67 | scope=scope,
68 | model=model
69 | ))
70 | else:
71 | permissions.append('{scope}_{resource}'.format(
72 | scope=scope,
73 | resource=p['resource_set_name']
74 | ))
75 | else:
76 | permissions.append(p['resource_set_name'])
77 |
78 | return permissions
79 | else:
80 | raise ImproperlyConfigured(
81 | 'Unsupported permission method configured for '
82 | 'Keycloak: {}'.format(settings.KEYCLOAK_PERMISSIONS_METHOD)
83 | )
84 |
85 | def has_perm(self, user_obj, perm, obj=None):
86 |
87 | if not user_obj.is_active:
88 | return False
89 |
90 | granted_perms = self.get_all_permissions(user_obj, obj)
91 | return perm in granted_perms
92 |
93 |
94 | class KeycloakAuthorizationCodeBackend(KeycloakAuthorizationBase):
95 |
96 | def authenticate(self, request, code, redirect_uri):
97 |
98 | if not hasattr(request, 'realm'):
99 | raise ImproperlyConfigured(
100 | 'Add BaseKeycloakMiddleware to middlewares')
101 |
102 | keycloak_openid_profile = django_keycloak.services\
103 | .oidc_profile.update_or_create_from_code(
104 | client=request.realm.client,
105 | code=code,
106 | redirect_uri=redirect_uri
107 | )
108 |
109 | return keycloak_openid_profile.user
110 |
111 |
112 | class KeycloakPasswordCredentialsBackend(KeycloakAuthorizationBase):
113 |
114 | def authenticate(self, request, username, password):
115 |
116 | if not hasattr(request, 'realm'):
117 | raise ImproperlyConfigured(
118 | 'Add BaseKeycloakMiddleware to middlewares')
119 |
120 | if not request.realm:
121 | # If request.realm does exist, but it is filled with None, we
122 | # can't authenticate using Keycloak
123 | return None
124 |
125 | try:
126 | keycloak_openid_profile = django_keycloak.services\
127 | .oidc_profile.update_or_create_from_password_credentials(
128 | client=request.realm.client,
129 | username=username,
130 | password=password
131 | )
132 | except KeycloakClientError:
133 | logger.debug('KeycloakPasswordCredentialsBackend: failed to '
134 | 'authenticate.')
135 | else:
136 | return keycloak_openid_profile.user
137 |
138 | return None
139 |
140 |
141 | class KeycloakIDTokenAuthorizationBackend(KeycloakAuthorizationBase):
142 |
143 | def authenticate(self, request, access_token):
144 |
145 | if not hasattr(request, 'realm'):
146 | raise ImproperlyConfigured(
147 | 'Add BaseKeycloakMiddleware to middlewares')
148 |
149 | try:
150 | oidc_profile = django_keycloak.services.oidc_profile\
151 | .get_or_create_from_id_token(
152 | client=request.realm.client,
153 | id_token=access_token
154 | )
155 | except ExpiredSignatureError:
156 | # If the signature has expired.
157 | logger.debug('KeycloakBearerAuthorizationBackend: failed to '
158 | 'authenticate due to an expired access token.')
159 | except JWTClaimsError as e:
160 | logger.debug('KeycloakBearerAuthorizationBackend: failed to '
161 | 'authenticate due to failing claim checks: "%s"'
162 | % str(e))
163 | except JWTError:
164 | # The signature is invalid in any way.
165 | logger.debug('KeycloakBearerAuthorizationBackend: failed to '
166 | 'authenticate due to a malformed access token.')
167 | else:
168 | return oidc_profile.user
169 |
170 | return None
171 |
--------------------------------------------------------------------------------
/src/django_keycloak/factories.py:
--------------------------------------------------------------------------------
1 | import factory
2 |
3 | from django.contrib.auth import get_user_model
4 |
5 | from django_keycloak.models import (
6 | Client,
7 | OpenIdConnectProfile,
8 | Realm,
9 | Server
10 | )
11 |
12 |
13 | class UserFactory(factory.DjangoModelFactory):
14 |
15 | class Meta(object):
16 | model = get_user_model()
17 |
18 | username = factory.Faker('user_name')
19 |
20 |
21 | class ServerFactory(factory.DjangoModelFactory):
22 |
23 | class Meta(object):
24 | model = Server
25 |
26 | url = factory.Faker('url', schemes=['https'])
27 |
28 |
29 | class RealmFactory(factory.DjangoModelFactory):
30 |
31 | class Meta(object):
32 | model = Realm
33 |
34 | server = factory.SubFactory(ServerFactory)
35 |
36 | name = factory.Faker('slug')
37 |
38 | _certs = ''
39 | _well_known_oidc = '{}'
40 |
41 | client = factory.RelatedFactory('django_keycloak.factories.ClientFactory',
42 | 'realm')
43 |
44 |
45 | class OpenIdConnectProfileFactory(factory.DjangoModelFactory):
46 |
47 | class Meta(object):
48 | model = OpenIdConnectProfile
49 |
50 | sub = factory.Faker('uuid4')
51 | realm = factory.SubFactory(RealmFactory)
52 | user = factory.SubFactory(UserFactory)
53 |
54 |
55 | class ClientFactory(factory.DjangoModelFactory):
56 |
57 | class Meta(object):
58 | model = Client
59 |
60 | realm = factory.SubFactory(RealmFactory, client=None)
61 | service_account_profile = factory.SubFactory(
62 | OpenIdConnectProfileFactory,
63 | realm=factory.SelfAttribute('..realm')
64 | )
65 |
66 | client_id = factory.Faker('slug')
67 | secret = factory.Faker('uuid4')
68 |
--------------------------------------------------------------------------------
/src/django_keycloak/hashers.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 |
3 | from django.contrib.auth.hashers import PBKDF2PasswordHasher
4 |
5 |
6 | class PBKDF2SHA512PasswordHasher(PBKDF2PasswordHasher):
7 |
8 | algorithm = "pbkdf2_sha512"
9 | digest = hashlib.sha512
10 |
--------------------------------------------------------------------------------
/src/django_keycloak/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/management/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/management/commands/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/management/commands/keycloak_add_user.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import logging
4 |
5 | from django.core.management.base import BaseCommand
6 | from django.contrib.auth import get_user_model
7 |
8 | from django_keycloak.models import Realm
9 |
10 | import django_keycloak.services.users
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | def realm(name):
16 | try:
17 | return Realm.objects.get(name=name)
18 | except Realm.DoesNotExist:
19 | raise TypeError('Realm does not exist')
20 |
21 |
22 | def user(username):
23 | UserModel = get_user_model()
24 | try:
25 | return UserModel.objects.get(username=username)
26 | except UserModel.DoesNotExist:
27 | raise TypeError('User does not exist')
28 |
29 |
30 | class Command(BaseCommand):
31 |
32 | def add_arguments(self, parser):
33 | parser.add_argument('--realm', type=realm, required=True)
34 | parser.add_argument('--user', type=user, required=True)
35 |
36 | def handle(self, *args, **options):
37 | user = options['user']
38 | realm = options['realm']
39 |
40 | django_keycloak.services.users.add_user(client=realm.client, user=user)
41 |
--------------------------------------------------------------------------------
/src/django_keycloak/management/commands/keycloak_refresh_realm.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import logging
4 |
5 | from django.core.management.base import BaseCommand
6 |
7 | from django_keycloak.models import Realm
8 |
9 | import django_keycloak.services.realm
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | class Command(BaseCommand):
15 |
16 | def handle(self, *args, **options):
17 | for realm in Realm.objects.all():
18 | django_keycloak.services.realm.refresh_well_known_oidc(realm=realm)
19 | django_keycloak.services.realm.refresh_certs(realm=realm)
20 | logger.debug('Refreshed: {}'.format(realm))
21 |
--------------------------------------------------------------------------------
/src/django_keycloak/management/commands/keycloak_sync_resources.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import logging
4 |
5 | from django.core.management.base import BaseCommand
6 |
7 | from django_keycloak.models import Client
8 |
9 | import django_keycloak.services.uma
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def client(client_id):
15 | try:
16 | return Client.objects.get(client_id=client_id)
17 | except Client.DoesNotExist:
18 | raise TypeError('Client does not exist')
19 |
20 |
21 | class Command(BaseCommand):
22 |
23 | def add_arguments(self, parser):
24 | parser.add_argument('--client', type=client, required=False)
25 |
26 | def handle(self, *args, **options):
27 | client = options.get('client')
28 |
29 | if client:
30 | django_keycloak.services.uma.synchronize_client(client=client)
31 | else:
32 | for client in Client.objects.all():
33 | django_keycloak.services.uma.synchronize_client(client=client)
34 |
--------------------------------------------------------------------------------
/src/django_keycloak/middleware.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from django.conf import settings
4 | from django.contrib.auth import authenticate
5 | from django.contrib.auth.models import AnonymousUser
6 | from django.utils.deprecation import MiddlewareMixin
7 | from django.utils.functional import SimpleLazyObject
8 |
9 | from django_keycloak.models import Realm
10 | from django_keycloak.auth import get_remote_user
11 | from django_keycloak.response import HttpResponseNotAuthorized
12 |
13 |
14 | def get_realm(request):
15 | if not hasattr(request, '_cached_realm'):
16 | request._cached_realm = Realm.objects.first()
17 | return request._cached_realm
18 |
19 |
20 | def get_user(request, origin_user):
21 | # Check for the user as set by
22 | # django.contrib.auth.middleware.AuthenticationMiddleware
23 | if not isinstance(origin_user, AnonymousUser):
24 | return origin_user
25 |
26 | if not hasattr(request, '_cached_user'):
27 | request._cached_user = get_remote_user(request)
28 | return request._cached_user
29 |
30 |
31 | class BaseKeycloakMiddleware(MiddlewareMixin):
32 |
33 | set_session_state_cookie = True
34 |
35 | def process_request(self, request):
36 | """
37 | Adds Realm to request.
38 | :param request: django request
39 | """
40 | request.realm = SimpleLazyObject(lambda: get_realm(request))
41 |
42 | def process_response(self, request, response):
43 |
44 | if self.set_session_state_cookie:
45 | return self.set_session_state_cookie_(request, response)
46 |
47 | return response
48 |
49 | def set_session_state_cookie_(self, request, response):
50 |
51 | if not request.user.is_authenticated \
52 | or not hasattr(request.user, 'oidc_profile'):
53 | return response
54 |
55 | jwt = request.user.oidc_profile.jwt
56 | if not jwt:
57 | return response
58 |
59 | cookie_name = getattr(settings, 'KEYCLOAK_SESSION_STATE_COOKIE_NAME',
60 | 'session_state')
61 |
62 | # Set a browser readable cookie which expires when the refresh token
63 | # expires.
64 | response.set_cookie(
65 | cookie_name, value=jwt['session_state'],
66 | expires=request.user.oidc_profile.refresh_expires_before,
67 | httponly=False
68 | )
69 |
70 | return response
71 |
72 |
73 | class KeycloakStatelessBearerAuthenticationMiddleware(BaseKeycloakMiddleware):
74 |
75 | set_session_state_cookie = False
76 | header_key = "HTTP_AUTHORIZATION"
77 |
78 | def process_request(self, request):
79 | """
80 | Forces authentication on all requests except the URL's configured in
81 | the exempt setting.
82 | """
83 | super(KeycloakStatelessBearerAuthenticationMiddleware, self)\
84 | .process_request(request=request)
85 |
86 | if hasattr(settings, 'KEYCLOAK_BEARER_AUTHENTICATION_EXEMPT_PATHS'):
87 | path = request.path_info.lstrip('/')
88 |
89 | if any(re.match(m, path) for m in
90 | settings.KEYCLOAK_BEARER_AUTHENTICATION_EXEMPT_PATHS):
91 | return
92 |
93 | if self.header_key not in request.META:
94 | return HttpResponseNotAuthorized(
95 | attributes={'realm': request.realm.name})
96 |
97 | user = authenticate(
98 | request=request,
99 | access_token=request.META[self.header_key].split(' ')[1]
100 | )
101 |
102 | if user is None:
103 | return HttpResponseNotAuthorized(
104 | attributes={'realm': request.realm.name})
105 | else:
106 | request.user = user
107 |
108 |
109 | class RemoteUserAuthenticationMiddleware(MiddlewareMixin):
110 | set_session_state_cookie = False
111 |
112 | def process_request(self, request):
113 | """
114 | Adds user to the request when authorized user is found in the session
115 | :param django.http.request.HttpRequest request: django request
116 | """
117 | origin_user = getattr(request, 'user', None)
118 |
119 | request.user = SimpleLazyObject(lambda: get_user(
120 | request,
121 | origin_user=origin_user
122 | ))
123 |
--------------------------------------------------------------------------------
/src/django_keycloak/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.2 on 2018-03-15 21:15
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 | import uuid
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Client',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True,
22 | serialize=False, verbose_name='ID')),
23 | ('client_id', models.CharField(max_length=255)),
24 | ('secret', models.CharField(max_length=255)),
25 | ],
26 | ),
27 | migrations.CreateModel(
28 | name='Nonce',
29 | fields=[
30 | ('id', models.AutoField(auto_created=True, primary_key=True,
31 | serialize=False, verbose_name='ID')),
32 | ('state', models.UUIDField(default=uuid.uuid4, unique=True)),
33 | ('redirect_uri', models.CharField(max_length=255)),
34 | ('next_path', models.CharField(max_length=255, null=True)),
35 | ],
36 | ),
37 | migrations.CreateModel(
38 | name='OpenIdConnectProfile',
39 | fields=[
40 | ('id', models.AutoField(auto_created=True, primary_key=True,
41 | serialize=False, verbose_name='ID')),
42 | ('access_token', models.TextField(null=True)),
43 | ('expires_before', models.DateTimeField(null=True)),
44 | ('refresh_token', models.TextField(null=True)),
45 | ('refresh_expires_before', models.DateTimeField(null=True)),
46 | ('sub', models.CharField(max_length=255, unique=True)),
47 | ],
48 | options={
49 | 'abstract': False,
50 | 'swappable': 'KEYCLOAK_OIDC_PROFILE_MODEL',
51 | },
52 | ),
53 | migrations.CreateModel(
54 | name='Realm',
55 | fields=[
56 | ('id', models.AutoField(auto_created=True, primary_key=True,
57 | serialize=False, verbose_name='ID')),
58 | ('name', models.CharField(
59 | help_text='Name as known on the Keycloak server. This '
60 | 'name is used in the API paths of this Realm.',
61 | max_length=255, unique=True)),
62 | ('_certs', models.TextField()),
63 | ('_well_known_oidc', models.TextField(blank=True)),
64 | ],
65 | ),
66 | migrations.CreateModel(
67 | name='Role',
68 | fields=[
69 | ('id', models.AutoField(auto_created=True, primary_key=True,
70 | serialize=False, verbose_name='ID')),
71 | ('reference', models.CharField(max_length=50)),
72 | ('client', models.ForeignKey(
73 | on_delete=django.db.models.deletion.CASCADE,
74 | related_name='roles', to='django_keycloak.Client')),
75 | ('permission', models.ForeignKey(
76 | on_delete=django.db.models.deletion.CASCADE,
77 | to='auth.Permission')),
78 | ],
79 | ),
80 | migrations.CreateModel(
81 | name='Server',
82 | fields=[
83 | ('id', models.AutoField(auto_created=True, primary_key=True,
84 | serialize=False, verbose_name='ID')),
85 | ('url', models.CharField(max_length=255)),
86 | ('internal_url', models.CharField(
87 | blank=True,
88 | help_text='URL on internal netwerk calls. For example '
89 | 'when used with Docker Compose. Only supply '
90 | 'when internal calls should go to a different '
91 | 'url as the end-user will communicate with.',
92 | max_length=255, null=True)),
93 | ],
94 | ),
95 | migrations.AddField(
96 | model_name='realm',
97 | name='server',
98 | field=models.ForeignKey(
99 | on_delete=django.db.models.deletion.CASCADE,
100 | related_name='realms', to='django_keycloak.Server'),
101 | ),
102 | migrations.AddField(
103 | model_name='openidconnectprofile',
104 | name='realm',
105 | field=models.ForeignKey(
106 | on_delete=django.db.models.deletion.CASCADE,
107 | related_name='openid_profiles', to='django_keycloak.Realm'),
108 | ),
109 | migrations.AddField(
110 | model_name='openidconnectprofile',
111 | name='user',
112 | field=models.OneToOneField(
113 | on_delete=django.db.models.deletion.CASCADE,
114 | related_name='oidc_profile', to=settings.AUTH_USER_MODEL),
115 | ),
116 | migrations.AddField(
117 | model_name='client',
118 | name='realm',
119 | field=models.OneToOneField(
120 | on_delete=django.db.models.deletion.CASCADE,
121 | related_name='client', to='django_keycloak.Realm'),
122 | ),
123 | migrations.AddField(
124 | model_name='client',
125 | name='service_account',
126 | field=models.OneToOneField(
127 | null=True, on_delete=django.db.models.deletion.CASCADE,
128 | related_name='keycloak_client', to=settings.AUTH_USER_MODEL),
129 | ),
130 | migrations.AlterUniqueTogether(
131 | name='role',
132 | unique_together={('client', 'permission')},
133 | ),
134 | ]
135 |
--------------------------------------------------------------------------------
/src/django_keycloak/migrations/0002_auto_20180322_2059.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.3 on 2018-03-22 20:59
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('django_keycloak', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='ExchangedToken',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True,
18 | serialize=False, verbose_name='ID')),
19 | ('access_token', models.TextField(null=True)),
20 | ('expires_before', models.DateTimeField(null=True)),
21 | ('refresh_token', models.TextField(null=True)),
22 | ('refresh_expires_before', models.DateTimeField(null=True)),
23 | ('oidc_profile', models.ForeignKey(
24 | on_delete=django.db.models.deletion.CASCADE,
25 | to='django_keycloak.OpenIdConnectProfile')),
26 | ],
27 | ),
28 | migrations.CreateModel(
29 | name='RemoteClient',
30 | fields=[
31 | ('id', models.AutoField(auto_created=True, primary_key=True,
32 | serialize=False, verbose_name='ID')),
33 | ('name', models.CharField(max_length=255)),
34 | ('realm', models.ForeignKey(
35 | on_delete=django.db.models.deletion.CASCADE,
36 | related_name='remote_clients',
37 | to='django_keycloak.Realm'
38 | )),
39 | ],
40 | ),
41 | migrations.AddField(
42 | model_name='exchangedtoken',
43 | name='remote_client',
44 | field=models.ForeignKey(
45 | on_delete=django.db.models.deletion.CASCADE,
46 | related_name='exchanged_tokens',
47 | to='django_keycloak.RemoteClient'
48 | ),
49 | ),
50 | migrations.AlterUniqueTogether(
51 | name='exchangedtoken',
52 | unique_together={('oidc_profile', 'remote_client')},
53 | ),
54 | ]
55 |
--------------------------------------------------------------------------------
/src/django_keycloak/migrations/0003_auto_20190204_1949.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.5 on 2019-02-04 19:49
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('django_keycloak', '0002_auto_20180322_2059'),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='RemoteUserOpenIdConnectProfile',
17 | fields=[
18 | ('id', models.AutoField(auto_created=True, primary_key=True,
19 | serialize=False, verbose_name='ID')),
20 | ('access_token', models.TextField(null=True)),
21 | ('expires_before', models.DateTimeField(null=True)),
22 | ('refresh_token', models.TextField(null=True)),
23 | ('refresh_expires_before', models.DateTimeField(null=True)),
24 | ('sub', models.CharField(max_length=255, unique=True)),
25 | ('realm', models.ForeignKey(
26 | on_delete=django.db.models.deletion.CASCADE,
27 | related_name='openid_profiles',
28 | to='django_keycloak.Realm'
29 | )),
30 | ],
31 | options={
32 | 'abstract': False,
33 | 'swappable': 'KEYCLOAK_OIDC_PROFILE_MODEL',
34 | },
35 | ),
36 | migrations.AlterField(
37 | model_name='exchangedtoken',
38 | name='oidc_profile',
39 | field=models.ForeignKey(
40 | on_delete=django.db.models.deletion.CASCADE,
41 | to=settings.KEYCLOAK_OIDC_PROFILE_MODEL
42 | ),
43 | ),
44 | ]
45 |
--------------------------------------------------------------------------------
/src/django_keycloak/migrations/0004_client_service_account_profile.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.5 on 2019-02-19 13:23
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('django_keycloak', '0003_auto_20190204_1949'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='client',
17 | name='service_account_profile',
18 | field=models.OneToOneField(
19 | null=True,
20 | on_delete=django.db.models.deletion.CASCADE,
21 | to=settings.KEYCLOAK_OIDC_PROFILE_MODEL
22 | ),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/src/django_keycloak/migrations/0005_auto_20190219_2002.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.5 on 2019-02-19 20:02
2 |
3 | from django.db import migrations
4 |
5 |
6 | def forward(apps, schema_editor):
7 | Client = apps.get_model('django_keycloak', 'Client')
8 | for client in Client.objects.filter(service_account__isnull=False):
9 | client.service_account_profile = client.service_account.oidc_profile
10 | client.save()
11 |
12 |
13 | def backward(apps, schema_editor):
14 | Client = apps.get_model('django_keycloak', 'Client')
15 | for client in Client.objects.filter(service_account_profile__isnull=False):
16 | client.service_account = client.service_account_profile.user
17 | client.save()
18 |
19 |
20 | class Migration(migrations.Migration):
21 |
22 | dependencies = [
23 | ('django_keycloak', '0004_client_service_account_profile'),
24 | ]
25 |
26 | operations = [
27 | migrations.RunPython(forward, backward),
28 | ]
29 |
--------------------------------------------------------------------------------
/src/django_keycloak/migrations/0006_remove_client_service_account.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.5 on 2019-02-19 20:22
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('django_keycloak', '0005_auto_20190219_2002'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='client',
15 | name='service_account',
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/src/django_keycloak/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/migrations/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/models.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import uuid
4 |
5 | from django.contrib.auth.models import Permission
6 | from django.db import models
7 | from django.conf import settings
8 | from django.utils import timezone
9 | from django.utils.functional import cached_property
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | class Server(models.Model):
15 |
16 | url = models.CharField(max_length=255)
17 |
18 | internal_url = models.CharField(
19 | max_length=255,
20 | blank=True,
21 | null=True,
22 | help_text='URL on internal netwerk calls. For example when used with '
23 | 'Docker Compose. Only supply when internal calls should go '
24 | 'to a different url as the end-user will communicate with.'
25 | )
26 |
27 | def __str__(self):
28 | return self.url
29 |
30 |
31 | class Realm(models.Model):
32 |
33 | server = models.ForeignKey(Server, related_name='realms',
34 | on_delete=models.CASCADE)
35 |
36 | name = models.CharField(max_length=255, unique=True,
37 | help_text='Name as known on the Keycloak server. '
38 | 'This name is used in the API paths '
39 | 'of this Realm.')
40 | _certs = models.TextField()
41 |
42 | @property
43 | def certs(self):
44 | return json.loads(self._certs)
45 |
46 | @certs.setter
47 | def certs(self, content):
48 | self._certs = json.dumps(content)
49 |
50 | _well_known_oidc = models.TextField(blank=True)
51 |
52 | @property
53 | def well_known_oidc(self):
54 | return json.loads(self._well_known_oidc)
55 |
56 | @well_known_oidc.setter
57 | def well_known_oidc(self, content):
58 | self._well_known_oidc = json.dumps(content)
59 |
60 | _keycloak_realm = None
61 |
62 | @cached_property
63 | def realm_api_client(self):
64 | """
65 | :rtype: keycloak.realm.Realm
66 | """
67 | if self._keycloak_realm is None:
68 | import django_keycloak.services.realm
69 | self._keycloak_realm = django_keycloak.services.realm.\
70 | get_realm_api_client(realm=self)
71 | return self._keycloak_realm
72 |
73 | def __str__(self):
74 | return self.name
75 |
76 |
77 | class Client(models.Model):
78 |
79 | realm = models.OneToOneField(Realm, related_name='client',
80 | on_delete=models.CASCADE)
81 |
82 | client_id = models.CharField(max_length=255)
83 | secret = models.CharField(max_length=255)
84 |
85 | service_account_profile = models.OneToOneField(
86 | settings.KEYCLOAK_OIDC_PROFILE_MODEL,
87 | on_delete=models.CASCADE,
88 | null=True
89 | )
90 |
91 | @cached_property
92 | def admin_api_client(self):
93 | """
94 | :rtype: keycloak.admin.KeycloakAdmin
95 | """
96 | import django_keycloak.services.client
97 | return django_keycloak.services.client.get_admin_client(client=self)
98 |
99 | @cached_property
100 | def openid_api_client(self):
101 | """
102 | :rtype: keycloak.openid_connect.KeycloakOpenidConnect
103 | """
104 | import django_keycloak.services.client
105 | return django_keycloak.services.client.get_openid_client(client=self)
106 |
107 | @cached_property
108 | def authz_api_client(self):
109 | """
110 | :rtype: keycloak.authz.KeycloakAuthz
111 | """
112 | import django_keycloak.services.client
113 | return django_keycloak.services.client.get_authz_api_client(
114 | client=self)
115 |
116 | @cached_property
117 | def uma1_api_client(self):
118 | """
119 | :rtype: keycloak.uma1.KeycloakUMA1
120 | """
121 | import django_keycloak.services.client
122 | return django_keycloak.services.client.get_uma1_client(client=self)
123 |
124 | def __str__(self):
125 | return self.client_id
126 |
127 |
128 | class Role(models.Model):
129 |
130 | client = models.ForeignKey('django_keycloak.Client',
131 | related_name='roles',
132 | on_delete=models.CASCADE)
133 | permission = models.ForeignKey(Permission,
134 | on_delete=models.CASCADE)
135 | reference = models.CharField(max_length=50)
136 |
137 | class Meta(object):
138 | unique_together = (
139 | ('client', 'permission')
140 | )
141 |
142 |
143 | class TokenModelAbstract(models.Model):
144 |
145 | access_token = models.TextField(null=True)
146 | expires_before = models.DateTimeField(null=True)
147 |
148 | refresh_token = models.TextField(null=True)
149 | refresh_expires_before = models.DateTimeField(null=True)
150 |
151 | class Meta(object):
152 | abstract = True
153 |
154 | @property
155 | def is_active(self):
156 | if not self.access_token or not self.expires_before:
157 | return False
158 |
159 | return self.expires_before > timezone.now()
160 |
161 |
162 | class OpenIdConnectProfileAbstract(TokenModelAbstract):
163 |
164 | sub = models.CharField(max_length=255, unique=True)
165 |
166 | realm = models.ForeignKey('django_keycloak.Realm',
167 | related_name='openid_profiles',
168 | on_delete=models.CASCADE)
169 |
170 | class Meta(object):
171 | abstract = True
172 |
173 | @property
174 | def jwt(self):
175 | """
176 | :rtype: dict
177 | """
178 | if not self.is_active:
179 | return None
180 | client = self.realm.client
181 | return client.openid_api_client.decode_token(
182 | token=self.access_token,
183 | key=client.realm.certs,
184 | algorithms=client.openid_api_client.well_known[
185 | 'id_token_signing_alg_values_supported']
186 | )
187 |
188 |
189 | class RemoteUserOpenIdConnectProfile(OpenIdConnectProfileAbstract):
190 |
191 | is_remote = True
192 | _user = None
193 |
194 | class Meta(OpenIdConnectProfileAbstract.Meta):
195 | swappable = 'KEYCLOAK_OIDC_PROFILE_MODEL'
196 |
197 | def get_user(self):
198 | if self._user is None:
199 | import django_keycloak.services.oidc_profile
200 | self._user = django_keycloak.services.oidc_profile. \
201 | get_remote_user_from_profile(
202 | oidc_profile=self
203 | )
204 | return self._user
205 |
206 | def set_user(self, user):
207 | import django_keycloak.services.oidc_profile
208 | RemoteUserModel = django_keycloak.services.oidc_profile\
209 | .get_remote_user_model()
210 | if not isinstance(user, RemoteUserModel):
211 | raise RuntimeError('Can\'t set a non-remote user to the {}'.format(
212 | type(self)))
213 |
214 | self._user = user
215 |
216 | user = property(get_user, set_user)
217 |
218 |
219 | class OpenIdConnectProfile(OpenIdConnectProfileAbstract):
220 |
221 | is_remote = False
222 |
223 | user = models.OneToOneField(settings.AUTH_USER_MODEL,
224 | related_name='oidc_profile',
225 | on_delete=models.CASCADE)
226 |
227 | class Meta(RemoteUserOpenIdConnectProfile.Meta):
228 | swappable = 'KEYCLOAK_OIDC_PROFILE_MODEL'
229 |
230 |
231 | class Nonce(models.Model):
232 |
233 | state = models.UUIDField(default=uuid.uuid4, unique=True)
234 | redirect_uri = models.CharField(max_length=255)
235 | next_path = models.CharField(max_length=255, null=True)
236 |
237 |
238 | class ExchangedToken(TokenModelAbstract):
239 |
240 | oidc_profile = models.ForeignKey(
241 | settings.KEYCLOAK_OIDC_PROFILE_MODEL,
242 | on_delete=models.CASCADE
243 | )
244 | remote_client = models.ForeignKey('django_keycloak.RemoteClient',
245 | related_name='exchanged_tokens',
246 | on_delete=models.CASCADE)
247 |
248 | class Meta(object):
249 | unique_together = (
250 | ('oidc_profile', 'remote_client'),
251 | )
252 |
253 |
254 | class RemoteClient(models.Model):
255 |
256 | name = models.CharField(max_length=255)
257 | realm = models.ForeignKey('django_keycloak.Realm',
258 | related_name='remote_clients',
259 | on_delete=models.CASCADE)
260 |
--------------------------------------------------------------------------------
/src/django_keycloak/remote_user.py:
--------------------------------------------------------------------------------
1 | from django.contrib import auth
2 | from django.core.exceptions import PermissionDenied
3 |
4 | from django_keycloak.models import RemoteUserOpenIdConnectProfile
5 |
6 |
7 | class KeycloakRemoteUser(object):
8 | """
9 | A class based on django.contrib.auth.models.User.
10 | See https://docs.djangoproject.com/en/2.0/ref/contrib/auth/
11 | #django.contrib.auth.models.User
12 | """
13 |
14 | username = ''
15 | first_name = ''
16 | last_name = ''
17 | email = ''
18 | password = ''
19 | groups = []
20 | user_permissions = []
21 |
22 | _last_login = None
23 |
24 | def __init__(self, userinfo):
25 | """
26 | Create KeycloakRemoteUser from userinfo and oidc_profile.
27 | :param dict userinfo: the userinfo as retrieved from the OIDC provider
28 | """
29 | self.username = userinfo.get('preferred_username') or userinfo['sub']
30 | self.email = userinfo.get('email', '')
31 | self.first_name = userinfo.get('given_name', '')
32 | self.last_name = userinfo.get('family_name', '')
33 | self.sub = userinfo['sub']
34 |
35 | def __str__(self):
36 | return self.username
37 |
38 | @property
39 | def pk(self):
40 | """
41 | Since the BaseAbstractUser is a model, every instance needs a primary
42 | key. The Django authentication backend requires this.
43 | """
44 | return 0
45 |
46 | @property
47 | def identifier(self):
48 | """
49 | Identifier used for session storage.
50 | :rtype: str
51 | """
52 | return self.sub
53 |
54 | @property
55 | def is_staff(self):
56 | """
57 | :rtype: bool
58 | :return: whether the user is a staff member or not, defaults to False
59 | """
60 | return False
61 |
62 | @property
63 | def is_active(self):
64 | """
65 | :rtype: bool
66 | :return: whether the user is active or not, defaults to True
67 | """
68 | return True
69 |
70 | @property
71 | def is_superuser(self):
72 | """
73 | :rtype: bool
74 | :return: whether the user is a superuser or not, defaults to False
75 | """
76 | return False
77 |
78 | @property
79 | def last_login(self):
80 | """
81 | :rtype: Datetime
82 | :return: the date and time of the last login
83 | """
84 | return self._last_login
85 |
86 | @last_login.setter
87 | def last_login(self, content):
88 | """
89 | :param datetime content:
90 | """
91 | self._last_login = content
92 |
93 | @property
94 | def is_authenticated(self):
95 | """
96 | Read-only attribute which is always True.
97 | See https://docs.djangoproject.com/en/2.0/ref/contrib/auth/
98 | #django.contrib.auth.models.User.is_authenticated
99 | :return:
100 | """
101 | return True
102 |
103 | @property
104 | def is_anonymous(self):
105 | """
106 | Read-only attribute which is always False.
107 | See https://docs.djangoproject.com/en/2.0/ref/contrib/auth/
108 | #django.contrib.auth.models.User.is_anonymous
109 | :return:
110 | """
111 | return False
112 |
113 | def get_username(self):
114 | """
115 | Get the username
116 | :return: username
117 | """
118 | return self.username
119 |
120 | def get_full_name(self):
121 | """
122 | Get the full name (first name + last name) of the user.
123 | :return: the first name and last name of the user
124 | """
125 | return "{first} {last}".format(first=self.first_name,
126 | last=self.last_name)
127 |
128 | def get_short_name(self):
129 | """
130 | Get the first name of the user.
131 | :return: first name
132 | """
133 | return self.first_name
134 |
135 | def get_group_permissions(self, obj=None):
136 | pass
137 |
138 | def get_all_permissions(self, obj=None):
139 | """
140 | Logic from django.contrib.auth.models._user_get_all_permissions
141 | :param perm:
142 | :param obj:
143 | :return:
144 | """
145 | permissions = set()
146 | for backend in auth.get_backends():
147 | # Excluding Django.contrib.auth backends since they are not
148 | # compatible with non-db-backed permissions.
149 | if hasattr(backend, "get_all_permissions") \
150 | and not backend.__module__.startswith('django.'):
151 | permissions.update(backend.get_all_permissions(self, obj))
152 | return permissions
153 |
154 | @property
155 | def oidc_profile(self):
156 | """
157 | Get the related OIDC Profile for this user.
158 | :rtype: django_keycloak.models.RemoteUserOpenIdConnectProfile
159 | :return: OpenID Connect Profile
160 | """
161 | try:
162 | return RemoteUserOpenIdConnectProfile.objects.get(sub=self.sub)
163 | except RemoteUserOpenIdConnectProfile.DoesNotExist:
164 | return None
165 |
166 | def has_perm(self, perm, obj=None):
167 | """
168 | Logic from django.contrib.auth.models._user_has_perm
169 | :param perm:
170 | :param obj:
171 | :return:
172 | """
173 | for backend in auth.get_backends():
174 | if not hasattr(backend, 'has_perm') \
175 | or backend.__module__.startswith('django.contrib.auth'):
176 | continue
177 | try:
178 | if backend.has_perm(self, perm, obj):
179 | return True
180 | except PermissionDenied:
181 | return False
182 | return False
183 |
184 | def has_perms(self, perm_list, obj=None):
185 | return all(self.has_perm(perm, obj) for perm in perm_list)
186 |
187 | def has_module_perms(self, module):
188 | """
189 | Logic from django.contrib.auth.models._user_has_module_perms
190 | :param module:
191 | :return:
192 | """
193 | for backend in auth.get_backends():
194 | if not hasattr(backend, 'has_module_perms'):
195 | continue
196 | try:
197 | if backend.has_module_perms(self, module):
198 | return True
199 | except PermissionDenied:
200 | return False
201 | return False
202 |
203 | def email_user(self, subject, message, from_email=None, **kwargs):
204 | raise NotImplementedError('This feature is not implemented by default,'
205 | ' extend this class to implement')
206 |
207 | def save(self):
208 | """
209 | Normally implemented by django.db.models.Model
210 | :raises NotImplementedError: to remind that this is not a
211 | database-backed model and should not be used like one
212 | """
213 | raise NotImplementedError('This is not a database model')
214 |
--------------------------------------------------------------------------------
/src/django_keycloak/response.py:
--------------------------------------------------------------------------------
1 | from django.http.response import HttpResponse
2 |
3 |
4 | class HttpResponseNotAuthorized(HttpResponse):
5 | status_code = 401
6 |
7 | def __init__(self, content=b'', authorization_method='Bearer',
8 | attributes=None, *args, **kwargs):
9 | super(HttpResponseNotAuthorized, self).__init__(content, *args,
10 | **kwargs)
11 |
12 | attributes = attributes or {}
13 | attributes_str = ', '.join(
14 | [
15 | '{}="{}"'.format(key, value)
16 | for key, value in attributes.items()
17 | ]
18 | )
19 |
20 | self['WWW-Authenticate'] = '{} {}'.format(authorization_method,
21 | attributes_str)
22 |
--------------------------------------------------------------------------------
/src/django_keycloak/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/services/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/services/client.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from functools import partial
4 |
5 | from django.utils import timezone
6 |
7 | from django_keycloak.services.exceptions import TokensExpired
8 |
9 | import django_keycloak.services.oidc_profile
10 |
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | def get_keycloak_id(client):
16 | """
17 | Get internal Keycloak id for client configured in Realm
18 | :param django_keycloak.models.Realm realm:
19 | :return:
20 | """
21 | keycloak_clients = client.admin_api_client.realms.by_name(
22 | name=client.realm.name).clients.all()
23 | for keycloak_client in keycloak_clients:
24 | if keycloak_client['clientId'] == client.client_id:
25 | return keycloak_client['id']
26 |
27 | return None
28 |
29 |
30 | def get_authz_api_client(client):
31 | """
32 | :param django_keycloak.models.Client client:
33 | :rtype: keycloak.authz.KeycloakAuthz
34 | """
35 | return client.realm.realm_api_client.authz(client_id=client.client_id)
36 |
37 |
38 | def get_openid_client(client):
39 | """
40 | :param django_keycloak.models.Client client:
41 | :rtype: keycloak.openid_connect.KeycloakOpenidConnect
42 | """
43 | openid = client.realm.realm_api_client.open_id_connect(
44 | client_id=client.client_id,
45 | client_secret=client.secret
46 | )
47 |
48 | if client.realm._well_known_oidc:
49 | openid.well_known.contents = client.realm.well_known_oidc
50 |
51 | return openid
52 |
53 |
54 | def get_uma1_client(client):
55 | """
56 | :type client: django_keycloak.models.Client
57 | :rtype: keycloak.uma1.KeycloakUMA1
58 | """
59 | return client.realm.realm_api_client.uma1
60 |
61 |
62 | def get_admin_client(client):
63 | """
64 | Get the Keycloak admin client configured for given realm.
65 |
66 | :param django_keycloak.models.Client client:
67 | :rtype: keycloak.admin.KeycloakAdmin
68 | """
69 | token = partial(get_access_token, client)
70 | return client.realm.realm_api_client.admin.set_token(token=token)
71 |
72 |
73 | def get_service_account_profile(client):
74 | """
75 | Get service account for given client.
76 |
77 | :param django_keycloak.models.Client client:
78 | :rtype: django_keycloak.models.OpenIdConnectProfile
79 | """
80 |
81 | if client.service_account_profile:
82 | return client.service_account_profile
83 |
84 | token_response, initiate_time = get_new_access_token(client=client)
85 |
86 | oidc_profile = django_keycloak.services.oidc_profile._update_or_create(
87 | client=client,
88 | token_response=token_response,
89 | initiate_time=initiate_time)
90 |
91 | client.service_account_profile = oidc_profile
92 | client.save(update_fields=['service_account_profile'])
93 |
94 | return oidc_profile
95 |
96 |
97 | def get_new_access_token(client):
98 | """
99 | Get client access_token
100 |
101 | :param django_keycloak.models.Client client:
102 | :rtype: str
103 | """
104 | scope = 'realm-management openid'
105 |
106 | initiate_time = timezone.now()
107 | token_response = client.openid_api_client.client_credentials(scope=scope)
108 |
109 | return token_response, initiate_time
110 |
111 |
112 | def get_access_token(client):
113 | """
114 | Get access token from client's service account.
115 | :param django_keycloak.models.Client client:
116 | :rtype: str
117 | """
118 |
119 | oidc_profile = get_service_account_profile(client=client)
120 |
121 | try:
122 | return django_keycloak.services.oidc_profile.get_active_access_token(
123 | oidc_profile=oidc_profile)
124 | except TokensExpired:
125 | token_reponse, initiate_time = get_new_access_token(client=client)
126 | oidc_profile = django_keycloak.services.oidc_profile.update_tokens(
127 | token_model=oidc_profile,
128 | token_response=token_reponse,
129 | initiate_time=initiate_time
130 | )
131 | return oidc_profile.access_token
132 |
--------------------------------------------------------------------------------
/src/django_keycloak/services/exceptions.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class KeycloakOpenIdProfileNotFound(Exception):
4 | pass
5 |
6 |
7 | class TokensExpired(Exception):
8 | pass
9 |
--------------------------------------------------------------------------------
/src/django_keycloak/services/permissions.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.contrib.auth.models import Permission
4 | from requests.exceptions import HTTPError
5 |
6 | import django_keycloak.services.client
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | def synchronize(client):
12 | """
13 | :param django_keycloak.models.Client client:
14 | :return:
15 | """
16 | keycloak_client_id = django_keycloak.services.client.get_keycloak_id(
17 | client=client)
18 |
19 | role_api = client.admin_api_client.realms.by_name(client.realm.name)\
20 | .clients.by_id(keycloak_client_id).roles
21 |
22 | for permission in Permission.objects.all():
23 |
24 | try:
25 | role_api.create(name=permission.codename,
26 | description=permission.name)
27 | except HTTPError as e:
28 | if e.response.status_code != 409:
29 | raise
30 |
31 | else:
32 | continue
33 |
34 | # Update role
35 | role_api.by_name(permission.codename) \
36 | .update(name=permission.codename, description=permission.name)
37 |
--------------------------------------------------------------------------------
/src/django_keycloak/services/realm.py:
--------------------------------------------------------------------------------
1 | from keycloak.realm import KeycloakRealm
2 |
3 | try:
4 | from urllib.parse import urlparse
5 | except ImportError:
6 | from urlparse import urlparse
7 |
8 |
9 | def get_realm_api_client(realm):
10 | """
11 | :param django_keycloak.models.Realm realm:
12 | :return keycloak.realm.Realm:
13 | """
14 | headers = {}
15 | server_url = realm.server.url
16 | if realm.server.internal_url:
17 | # An internal URL is configured. We add some additional settings to let
18 | # Keycloak think that we access it using the server_url.
19 | server_url = realm.server.internal_url
20 | parsed_url = urlparse(realm.server.url)
21 | headers['Host'] = parsed_url.netloc
22 |
23 | if parsed_url.scheme == 'https':
24 | headers['X-Forwarded-Proto'] = 'https'
25 |
26 | return KeycloakRealm(server_url=server_url, realm_name=realm.name,
27 | headers=headers)
28 |
29 |
30 | def refresh_certs(realm):
31 | """
32 | :param django_keycloak.models.Realm realm:
33 | :rtype django_keycloak.models.Realm
34 | """
35 | realm.certs = realm.client.openid_api_client.certs()
36 | realm.save(update_fields=['_certs'])
37 | return realm
38 |
39 |
40 | def refresh_well_known_oidc(realm):
41 | """
42 | Refresh Open ID Connect .well-known
43 |
44 | :param django_keycloak.models.Realm realm:
45 | :rtype django_keycloak.models.Realm
46 | """
47 | server_url = realm.server.internal_url or realm.server.url
48 |
49 | # While fetching the well_known we should not use the prepared URL
50 | openid_api_client = KeycloakRealm(
51 | server_url=server_url,
52 | realm_name=realm.name
53 | ).open_id_connect(client_id='', client_secret='')
54 |
55 | realm.well_known_oidc = openid_api_client.well_known.contents
56 | realm.save(update_fields=['_well_known_oidc'])
57 | return realm
58 |
59 |
60 | def get_issuer(realm):
61 | """
62 | Get correct issuer to validate the JWT against. If an internal URL is
63 | configured for the server it will be replaced with the public one.
64 |
65 | :param django_keycloak.models.Realm realm:
66 | :return: issuer
67 | :rtype: str
68 | """
69 | issuer = realm.well_known_oidc['issuer']
70 | if realm.server.internal_url:
71 | return issuer.replace(realm.server.internal_url, realm.server.url, 1)
72 | return issuer
73 |
--------------------------------------------------------------------------------
/src/django_keycloak/services/remote_client.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.utils import timezone
4 |
5 | from django_keycloak.models import ExchangedToken
6 |
7 | import django_keycloak.services.oidc_profile
8 |
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | def exchange_token(oidc_profile, remote_client):
14 | """
15 | Exchange access token from OpenID Connect profile for a token of given
16 | remote client.
17 |
18 | :param django_keycloak.models.OpenIdConnectProfile oidc_profile:
19 | :param django_keycloak.models.RemoteClient remote_client:
20 | :rtype: dict
21 | """
22 | active_access_token = django_keycloak.services.oidc_profile\
23 | .get_active_access_token(oidc_profile=oidc_profile)
24 |
25 | # http://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange
26 | return oidc_profile.realm.client.openid_api_client.token_exchange(
27 | audience=remote_client.name,
28 | subject_token=active_access_token,
29 | requested_token_type='urn:ietf:params:oauth:token-type:refresh_token'
30 | )
31 |
32 |
33 | def get_active_remote_client_token(oidc_profile, remote_client):
34 | """
35 | Get an active remote client token. Exchange when not available or expired.
36 |
37 | :param django_keycloak.models.OpenIdConnectProfile oidc_profile:
38 | :param django_keycloak.models.RemoteClient remote_client:
39 | :rtype: str
40 | """
41 | exchanged_token, _ = ExchangedToken.objects.get_or_create(
42 | oidc_profile=oidc_profile,
43 | remote_client=remote_client
44 | )
45 |
46 | initiate_time = timezone.now()
47 |
48 | if exchanged_token.refresh_expires_before is None \
49 | or initiate_time > exchanged_token.refresh_expires_before \
50 | or initiate_time > exchanged_token.expires_before:
51 | token_response = exchange_token(oidc_profile, remote_client)
52 |
53 | exchanged_token = django_keycloak.services.oidc_profile.update_tokens(
54 | token_model=exchanged_token,
55 | token_response=token_response,
56 | initiate_time=initiate_time
57 | )
58 |
59 | return exchanged_token.access_token
60 |
--------------------------------------------------------------------------------
/src/django_keycloak/services/uma.py:
--------------------------------------------------------------------------------
1 | from django.apps.registry import apps
2 | from django.utils.text import slugify
3 |
4 | from keycloak.exceptions import KeycloakClientError
5 |
6 | import django_keycloak.services.client
7 |
8 |
9 | def synchronize_client(client):
10 | """
11 | Synchronize all models as resources for a client.
12 |
13 | :type client: django_keycloak.models.Client
14 | """
15 | for app_config in apps.get_app_configs():
16 | synchronize_resources(
17 | client=client,
18 | app_config=app_config
19 | )
20 |
21 |
22 | def synchronize_resources(client, app_config):
23 | """
24 | Synchronize all resources (models) to the Keycloak server for given client
25 | and Django App.
26 |
27 | :type client: django_keycloak.models.Client
28 | :type app_config: django.apps.config.AppConfig
29 | """
30 |
31 | if not app_config.models_module:
32 | return
33 |
34 | uma1_client = client.uma1_api_client
35 |
36 | access_token = django_keycloak.services.client.get_access_token(
37 | client=client
38 | )
39 |
40 | for klass in app_config.get_models():
41 | scopes = _get_all_permissions(klass._meta)
42 |
43 | try:
44 | uma1_client.resource_set_create(
45 | token=access_token,
46 | name=klass._meta.label_lower,
47 | type='urn:{client}:resources:{model}'.format(
48 | client=slugify(client.client_id),
49 | model=klass._meta.label_lower
50 | ),
51 | scopes=scopes
52 | )
53 | except KeycloakClientError as e:
54 | if e.original_exc.response.status_code != 409:
55 | raise
56 |
57 |
58 | def _get_all_permissions(meta):
59 | """
60 | :type meta: django.db.models.options.Options
61 | :rtype: list
62 | """
63 | return meta.default_permissions
64 |
--------------------------------------------------------------------------------
/src/django_keycloak/services/users.py:
--------------------------------------------------------------------------------
1 | import base64
2 |
3 |
4 | def credential_representation_from_hash(hash_, temporary=False):
5 | algorithm, hashIterations, salt, hashedSaltedValue = hash_.split('$')
6 |
7 | return {
8 | 'type': 'password',
9 | 'hashedSaltedValue': hashedSaltedValue,
10 | 'algorithm': algorithm.replace('_', '-'),
11 | 'hashIterations': int(hashIterations),
12 | 'salt': base64.b64encode(salt.encode()).decode('ascii').strip(),
13 | 'temporary': temporary
14 | }
15 |
16 |
17 | def add_user(client, user):
18 | """
19 | Create user in Keycloak based on a local user including password.
20 |
21 | :param django_keycloak.models.Client client:
22 | :param django.contrib.auth.models.User user:
23 | """
24 | credentials = credential_representation_from_hash(hash_=user.password)
25 |
26 | client.admin_api_client.realms.by_name(client.realm.name).users.create(
27 | username=user.username,
28 | credentials=credentials,
29 | first_name=user.first_name,
30 | last_name=user.last_name,
31 | email=user.email,
32 | enabled=user.is_active
33 | )
34 |
--------------------------------------------------------------------------------
/src/django_keycloak/templates/django_keycloak/includes/session_iframe_support.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/django_keycloak/templates/django_keycloak/session_iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/tests/backends/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/backends/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/tests/backends/keycloak_authorization_base/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/backends/keycloak_authorization_base/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/tests/backends/keycloak_authorization_base/test_get_keycloak_permissions.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, override_settings
2 |
3 | from django_keycloak.factories import OpenIdConnectProfileFactory
4 | from django_keycloak.tests.mixins import MockTestCaseMixin
5 | from django_keycloak.auth.backends import KeycloakAuthorizationBase
6 |
7 |
8 | @override_settings(KEYCLOAK_PERMISSIONS_METHOD='resource')
9 | class BackendsKeycloakAuthorizationBaseGetKeycloakPermissionsTestCase(
10 | MockTestCaseMixin, TestCase):
11 |
12 | def setUp(self):
13 | self.backend = KeycloakAuthorizationBase()
14 |
15 | self.profile = OpenIdConnectProfileFactory()
16 |
17 | self.setup_mock(
18 | 'django_keycloak.services.oidc_profile.get_entitlement',
19 | return_value={
20 | 'authorization': {
21 | 'permissions': [
22 | {
23 | 'resource_set_name': 'Resource',
24 | 'scopes': [
25 | 'Read',
26 | 'Update'
27 | ]
28 | },
29 | {
30 | 'resource_set_name': 'Resource2'
31 | }
32 | ]
33 | }
34 | })
35 |
36 | def test_get_keycloak_permissions(self):
37 | """
38 | Case: The permissions are requested from Keycloak, which are returned
39 | by get_entitlement as a decoded RPT.
40 | Expect: The permissions are extracted from the RPT and are returned
41 | in a list.
42 | """
43 | permissions = self.backend.get_keycloak_permissions(
44 | user_obj=self.profile.user)
45 |
46 | self.assertListEqual(
47 | ['Read_Resource', 'Update_Resource', 'Resource2'],
48 | permissions
49 | )
50 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/backends/keycloak_authorization_base/test_has_perm.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, override_settings
2 |
3 | from django_keycloak.factories import OpenIdConnectProfileFactory
4 | from django_keycloak.tests.mixins import MockTestCaseMixin
5 | from django_keycloak.auth.backends import KeycloakAuthorizationBase
6 |
7 |
8 | @override_settings(KEYCLOAK_PERMISSIONS_METHOD='resource')
9 | class BackendsKeycloakAuthorizationBaseHasPermTestCase(
10 | MockTestCaseMixin, TestCase):
11 |
12 | def setUp(self):
13 | self.backend = KeycloakAuthorizationBase()
14 |
15 | self.profile = OpenIdConnectProfileFactory(user__is_active=True)
16 |
17 | self.setup_mock(
18 | 'django_keycloak.services.oidc_profile.get_entitlement',
19 | return_value={
20 | 'authorization': {
21 | 'permissions': [
22 | {
23 | 'resource_set_name': 'Resource',
24 | 'scopes': [
25 | 'Read',
26 | 'Update'
27 | ]
28 | },
29 | {
30 | 'resource_set_name': 'Resource2'
31 | }
32 | ]
33 | }
34 | }
35 | )
36 |
37 | def test_resource_scope_should_have_permission(self):
38 | """
39 | Case: Permission is expected that is available to the user.
40 | Expected: Permission granted.
41 | """
42 | permission = self.backend.has_perm(
43 | user_obj=self.profile.user, perm='Read_Resource')
44 |
45 | self.assertTrue(permission)
46 |
47 | def test_resource_no_scope_should_not_have_permission(self):
48 | """"
49 | Case: Permission is formatted as resource only which does not exist as
50 | such in the RPT.
51 | Expected: Permission denied.
52 | """
53 | permission = self.backend.has_perm(
54 | user_obj=self.profile.user, perm='Resource')
55 |
56 | self.assertFalse(permission)
57 |
58 | def test_resource_other_scope_should_not_have_permission(self):
59 | """"
60 | Case: Permission is expected with a scope that is not available to
61 | the user according to the RPT.
62 | Expected: Permission denied.
63 | """
64 | permission = self.backend.has_perm(
65 | user_obj=self.profile.user, perm='Create_Resource')
66 |
67 | self.assertFalse(permission)
68 |
69 | def test_other_resource_other_scope_should_not_have_permission(self):
70 | """"
71 | Case: Permission is expected that is not available to the user
72 | according to the RPT.
73 | Expected: Permission denied.
74 | """
75 | permission = self.backend.has_perm(
76 | user_obj=self.profile.user, perm='OtherScope_OtherResource')
77 |
78 | self.assertFalse(permission)
79 |
80 | def test_resource_no_scope_should_have_permission(self):
81 | """"
82 | Case: Permission is expected with no scope provided, but scope is
83 | also not provided in the RPT.
84 | Expected: Permission granted.
85 | """
86 | permission = self.backend.has_perm(
87 | user_obj=self.profile.user, perm='Resource2')
88 |
89 | self.assertTrue(permission)
90 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/mixins.py:
--------------------------------------------------------------------------------
1 | import mock
2 |
3 |
4 | class MockTestCaseMixin(object):
5 |
6 | def __init__(self, *args, **kwargs):
7 | self._mocks = {}
8 |
9 | super(MockTestCaseMixin, self).__init__(*args, **kwargs)
10 |
11 | def setup_mock(self, target, autospec=True, **kwargs):
12 | if target in self._mocks:
13 | raise Exception('Target %s already patched', target)
14 |
15 | self._mocks[target] = mock.patch(target, autospec=autospec, **kwargs)
16 |
17 | return self._mocks[target].start()
18 |
19 | def tearDown(self):
20 | for mock_ in self._mocks.values():
21 | mock_.stop()
22 |
23 | return super(MockTestCaseMixin, self).tearDown()
24 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/services/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/tests/services/oidc_profile/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/services/oidc_profile/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/tests/services/oidc_profile/test_get_active_access_token.py:
--------------------------------------------------------------------------------
1 | import mock
2 |
3 | from datetime import datetime
4 |
5 | from django.test import TestCase
6 | from freezegun import freeze_time
7 | from keycloak.openid_connect import KeycloakOpenidConnect
8 |
9 | from django_keycloak.factories import OpenIdConnectProfileFactory
10 | from django_keycloak.tests.mixins import MockTestCaseMixin
11 |
12 | import django_keycloak.services.oidc_profile
13 |
14 |
15 | class ServicesKeycloakOpenIDProfileGetActiveAccessTokenTestCase(
16 | MockTestCaseMixin, TestCase):
17 |
18 | def setUp(self):
19 | self.oidc_profile = OpenIdConnectProfileFactory(
20 | access_token='access-token',
21 | expires_before=datetime(2018, 3, 5, 1, 0, 0),
22 | refresh_token='refresh-token',
23 | refresh_expires_before=datetime(2018, 3, 5, 2, 0, 0)
24 | )
25 | self.oidc_profile.realm.client.openid_api_client = mock.MagicMock(
26 | spec_set=KeycloakOpenidConnect)
27 | self.oidc_profile.realm.client.openid_api_client.refresh_token\
28 | .return_value = {
29 | 'access_token': 'new-access-token',
30 | 'expires_in': 600,
31 | 'refresh_token': 'new-refresh-token',
32 | 'refresh_expires_in': 3600
33 | }
34 |
35 | @freeze_time('2018-03-05 00:59:00')
36 | def test_not_expired(self):
37 | """
38 | Case: access token get fetched and is not yet expired
39 | Expected: current token is returned
40 | """
41 | access_token = django_keycloak.services.oidc_profile\
42 | .get_active_access_token(oidc_profile=self.oidc_profile)
43 |
44 | self.assertEqual(access_token, 'access-token')
45 | self.assertFalse(
46 | self.oidc_profile.realm.client.openid_api_client.refresh_token
47 | .called
48 | )
49 |
50 | @freeze_time('2018-03-05 01:01:00')
51 | def test_expired(self):
52 | """
53 | Case: access token get requested but current one is expired
54 | Expected: A new one get requested
55 | """
56 | access_token = django_keycloak.services.oidc_profile \
57 | .get_active_access_token(oidc_profile=self.oidc_profile)
58 |
59 | self.assertEqual(access_token, 'new-access-token')
60 | self.oidc_profile.realm.client.openid_api_client.refresh_token\
61 | .assert_called_once_with(refresh_token='refresh-token')
62 |
63 | self.oidc_profile.refresh_from_db()
64 | self.assertEqual(self.oidc_profile.access_token, 'new-access-token')
65 | self.assertEqual(self.oidc_profile.expires_before,
66 | datetime(2018, 3, 5, 1, 11, 0))
67 | self.assertEqual(self.oidc_profile.refresh_token, 'new-refresh-token')
68 | self.assertEqual(self.oidc_profile.refresh_expires_before,
69 | datetime(2018, 3, 5, 2, 1, 0))
70 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/services/oidc_profile/test_get_entitlement.py:
--------------------------------------------------------------------------------
1 | import mock
2 |
3 | from datetime import datetime
4 |
5 | from django.test import TestCase
6 | from keycloak.openid_connect import KeycloakOpenidConnect
7 | from keycloak.authz import KeycloakAuthz
8 |
9 | from django_keycloak.factories import OpenIdConnectProfileFactory
10 | from django_keycloak.tests.mixins import MockTestCaseMixin
11 |
12 | import django_keycloak.services.oidc_profile
13 |
14 |
15 | class ServicesKeycloakOpenIDProfileGetActiveAccessTokenTestCase(
16 | MockTestCaseMixin, TestCase):
17 |
18 | def setUp(self):
19 | self.mocked_get_active_access_token = self.setup_mock(
20 | 'django_keycloak.services.oidc_profile'
21 | '.get_active_access_token'
22 | )
23 |
24 | self.oidc_profile = OpenIdConnectProfileFactory(
25 | access_token='access-token',
26 | expires_before=datetime(2018, 3, 5, 1, 0, 0),
27 | refresh_token='refresh-token'
28 | )
29 | self.oidc_profile.realm.client.openid_api_client = mock.MagicMock(
30 | spec_set=KeycloakOpenidConnect)
31 | self.oidc_profile.realm.client.authz_api_client = mock.MagicMock(
32 | spec_set=KeycloakAuthz)
33 | self.oidc_profile.realm.client.authz_api_client.entitlement\
34 | .return_value = {
35 | 'rpt': 'RPT_VALUE'
36 | }
37 | self.oidc_profile.realm.certs = {'cert': 'cert-value'}
38 |
39 | def test(self):
40 | django_keycloak.services.oidc_profile.get_entitlement(
41 | oidc_profile=self.oidc_profile
42 | )
43 | self.oidc_profile.realm.client.authz_api_client.entitlement\
44 | .assert_called_once_with(
45 | token=self.mocked_get_active_access_token.return_value
46 | )
47 | self.oidc_profile.realm.client.openid_api_client.decode_token\
48 | .assert_called_once_with(
49 | token='RPT_VALUE',
50 | key=self.oidc_profile.realm.certs,
51 | options={
52 | 'verify_signature': True,
53 | 'exp': True,
54 | 'iat': True,
55 | 'aud': True
56 | }
57 | )
58 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/services/oidc_profile/test_get_or_create_from_id_token.py:
--------------------------------------------------------------------------------
1 | import mock
2 |
3 | from datetime import datetime
4 |
5 | from django.test import TestCase
6 | from keycloak.openid_connect import KeycloakOpenidConnect
7 |
8 | from django_keycloak.factories import ClientFactory, \
9 | OpenIdConnectProfileFactory, UserFactory
10 | from django_keycloak.tests.mixins import MockTestCaseMixin
11 |
12 | import django_keycloak.services.oidc_profile
13 |
14 |
15 | class ServicesOpenIDProfileGetOrCreateFromIdTokenTestCase(
16 | MockTestCaseMixin, TestCase):
17 |
18 | def setUp(self):
19 | self.client = ClientFactory(
20 | realm___certs='{}',
21 | realm___well_known_oidc='{"issuer": "https://issuer"}'
22 | )
23 | self.client.openid_api_client = mock.MagicMock(
24 | spec_set=KeycloakOpenidConnect)
25 | self.client.openid_api_client.well_known = {
26 | 'id_token_signing_alg_values_supported': ['signing-alg']
27 | }
28 | self.client.openid_api_client.decode_token.return_value = {
29 | 'sub': 'some-sub',
30 | 'email': 'test@example.com',
31 | 'given_name': 'Some given name',
32 | 'family_name': 'Some family name'
33 | }
34 |
35 | def test_create_with_new_user_new_profile(self):
36 | """
37 | Case: oidc profile is requested based on a provided id token.
38 | The user and profile do not exist yet.
39 | Expected: oidc profile and user are created with information from
40 | the id token.
41 | """
42 | profile = django_keycloak.services.oidc_profile. \
43 | get_or_create_from_id_token(
44 | client=self.client, id_token='some-id-token'
45 | )
46 |
47 | self.client.openid_api_client.decode_token.assert_called_with(
48 | token='some-id-token',
49 | key=dict(),
50 | algorithms=['signing-alg'],
51 | issuer='https://issuer'
52 | )
53 |
54 | self.assertEqual(profile.sub, 'some-sub')
55 | self.assertEqual(profile.user.username, 'some-sub')
56 | self.assertEqual(profile.user.email, 'test@example.com')
57 | self.assertEqual(profile.user.first_name, 'Some given name')
58 | self.assertEqual(profile.user.last_name, 'Some family name')
59 |
60 | def test_update_with_existing_profile_new_user(self):
61 | """
62 | Case: oidc profile is requested based on a provided id token.
63 | The profile exists, but the user doesn't.
64 | Expected: oidc user is created with information from the id token
65 | and linked to the profile.
66 | """
67 | existing_profile = OpenIdConnectProfileFactory(
68 | access_token='access-token',
69 | expires_before=datetime(2018, 3, 5, 1, 0, 0),
70 | refresh_token='refresh-token',
71 | sub='some-sub'
72 | )
73 |
74 | profile = django_keycloak.services.oidc_profile. \
75 | get_or_create_from_id_token(
76 | client=self.client, id_token='some-id-token'
77 | )
78 |
79 | self.client.openid_api_client.decode_token.assert_called_with(
80 | token='some-id-token',
81 | key=dict(),
82 | algorithms=['signing-alg'],
83 | issuer='https://issuer'
84 | )
85 |
86 | self.assertEqual(profile.sub, 'some-sub')
87 | self.assertEqual(profile.pk, existing_profile.pk)
88 | self.assertEqual(profile.user.username, 'some-sub')
89 | self.assertEqual(profile.user.email, 'test@example.com')
90 | self.assertEqual(profile.user.first_name, 'Some given name')
91 | self.assertEqual(profile.user.last_name, 'Some family name')
92 |
93 | def test_create_with_existing_user_new_profile(self):
94 | """
95 | Case: oidc profile is requested based on a provided id token.
96 | The user exists, but the profile doesn't.
97 | Expected: oidc profile is created and user is linked to the profile.
98 | """
99 | existing_user = UserFactory(
100 | username='some-sub'
101 | )
102 |
103 | profile = django_keycloak.services.oidc_profile.\
104 | get_or_create_from_id_token(
105 | client=self.client, id_token='some-id-token'
106 | )
107 |
108 | self.client.openid_api_client.decode_token.assert_called_with(
109 | token='some-id-token',
110 | key=dict(),
111 | algorithms=['signing-alg'],
112 | issuer='https://issuer'
113 | )
114 |
115 | self.assertEqual(profile.sub, 'some-sub')
116 | self.assertEqual(profile.user.pk, existing_user.pk)
117 | self.assertEqual(profile.user.username, 'some-sub')
118 | self.assertEqual(profile.user.email, 'test@example.com')
119 | self.assertEqual(profile.user.first_name, 'Some given name')
120 | self.assertEqual(profile.user.last_name, 'Some family name')
121 |
122 | def test_create_with_existing_user_existing_profile(self):
123 | """
124 | Case: oidc profile is requested based on a provided id token.
125 | The user and profile already exist.
126 | Expected: existing oidc profile is returned with existing user linked
127 | to it.
128 | """
129 | existing_user = UserFactory(
130 | username='some-sub'
131 | )
132 |
133 | existing_profile = OpenIdConnectProfileFactory(
134 | access_token='access-token',
135 | expires_before=datetime(2018, 3, 5, 1, 0, 0),
136 | refresh_token='refresh-token',
137 | sub='some-sub'
138 | )
139 |
140 | profile = django_keycloak.services.oidc_profile.\
141 | get_or_create_from_id_token(
142 | client=self.client, id_token='some-id-token'
143 | )
144 |
145 | self.client.openid_api_client.decode_token.assert_called_with(
146 | token='some-id-token',
147 | key=dict(),
148 | algorithms=['signing-alg'],
149 | issuer='https://issuer'
150 | )
151 |
152 | self.assertEqual(profile.pk, existing_profile.pk)
153 | self.assertEqual(profile.sub, 'some-sub')
154 | self.assertEqual(profile.user.pk, existing_user.pk)
155 | self.assertEqual(profile.user.username, 'some-sub')
156 | self.assertEqual(profile.user.email, 'test@example.com')
157 | self.assertEqual(profile.user.first_name, 'Some given name')
158 | self.assertEqual(profile.user.last_name, 'Some family name')
159 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/services/oidc_profile/test_update_or_create.py:
--------------------------------------------------------------------------------
1 | import mock
2 |
3 | from datetime import datetime
4 |
5 | from django.contrib.auth import get_user_model
6 | from django.test import TestCase
7 | from freezegun import freeze_time
8 | from keycloak.openid_connect import KeycloakOpenidConnect
9 |
10 | from django_keycloak.factories import ClientFactory
11 | from django_keycloak.models import OpenIdConnectProfile
12 | from django_keycloak.tests.mixins import MockTestCaseMixin
13 |
14 | import django_keycloak.services.oidc_profile
15 |
16 |
17 | class ServicesKeycloakOpenIDProfileUpdateOrCreateTestCase(MockTestCaseMixin,
18 | TestCase):
19 |
20 | def setUp(self):
21 | self.client = ClientFactory(
22 | realm___certs='{}',
23 | realm___well_known_oidc='{"issuer": "https://issuer"}'
24 | )
25 | self.client.openid_api_client = mock.MagicMock(
26 | spec_set=KeycloakOpenidConnect)
27 | self.client.openid_api_client.authorization_code.return_value = {
28 | 'id_token': 'id-token',
29 | 'expires_in': 600,
30 | 'refresh_expires_in': 3600,
31 | 'access_token': 'access-token',
32 | 'refresh_token': 'refresh-token'
33 | }
34 | self.client.openid_api_client.well_known = {
35 | 'id_token_signing_alg_values_supported': ['signing-alg']
36 | }
37 | self.client.openid_api_client.decode_token.return_value = {
38 | 'sub': 'some-sub',
39 | 'email': 'test@example.com',
40 | 'given_name': 'Some given name',
41 | 'family_name': 'Some family name'
42 | }
43 |
44 | @freeze_time('2018-03-01 00:00:00')
45 | def test_create(self):
46 | django_keycloak.services.oidc_profile.update_or_create_from_code(
47 | client=self.client,
48 | code='some-code',
49 | redirect_uri='https://redirect'
50 | )
51 | self.client.openid_api_client.authorization_code\
52 | .assert_called_once_with(code='some-code',
53 | redirect_uri='https://redirect')
54 | self.client.openid_api_client.decode_token.assert_called_once_with(
55 | token='id-token',
56 | key=dict(),
57 | algorithms=['signing-alg'],
58 | issuer='https://issuer'
59 | )
60 |
61 | profile = OpenIdConnectProfile.objects.get(sub='some-sub')
62 | self.assertEqual(profile.sub, 'some-sub'),
63 | self.assertEqual(profile.access_token, 'access-token')
64 | self.assertEqual(profile.refresh_token, 'refresh-token')
65 | self.assertEqual(profile.expires_before, datetime(
66 | year=2018, month=3, day=1, hour=0, minute=10, second=0
67 | ))
68 | self.assertEqual(profile.refresh_expires_before, datetime(
69 | year=2018, month=3, day=1, hour=1, minute=0, second=0
70 | ))
71 |
72 | user = profile.user
73 | self.assertEqual(user.username, 'some-sub')
74 | self.assertEqual(user.first_name, 'Some given name')
75 | self.assertEqual(user.last_name, 'Some family name')
76 |
77 | @freeze_time('2018-03-01 00:00:00')
78 | def test_update(self):
79 | UserModel = get_user_model()
80 | user = UserModel.objects.create(
81 | username='some-sub',
82 | email='',
83 | first_name='',
84 | last_name=''
85 | )
86 | profile = OpenIdConnectProfile.objects.create(
87 | realm=self.client.realm,
88 | sub='some-sub',
89 | user=user,
90 | access_token='another-access-token',
91 | expires_before=datetime.now(),
92 | refresh_token='another-refresh-token',
93 | refresh_expires_before=datetime.now()
94 | )
95 |
96 | django_keycloak.services.oidc_profile.update_or_create_from_code(
97 | client=self.client,
98 | code='some-code',
99 | redirect_uri='https://redirect'
100 | )
101 | self.client.openid_api_client.authorization_code\
102 | .assert_called_once_with(code='some-code',
103 | redirect_uri='https://redirect')
104 | self.client.openid_api_client.decode_token.assert_called_once_with(
105 | token='id-token',
106 | key=dict(),
107 | algorithms=['signing-alg'],
108 | issuer='https://issuer'
109 | )
110 |
111 | profile.refresh_from_db()
112 | self.assertEqual(profile.sub, 'some-sub')
113 | self.assertEqual(profile.access_token, 'access-token')
114 | self.assertEqual(profile.refresh_token, 'refresh-token')
115 | self.assertEqual(profile.expires_before, datetime(
116 | year=2018, month=3, day=1, hour=0, minute=10, second=0
117 | ))
118 | self.assertEqual(profile.refresh_expires_before, datetime(
119 | year=2018, month=3, day=1, hour=1, minute=0, second=0
120 | ))
121 |
122 | user = profile.user
123 | user.refresh_from_db()
124 | self.assertEqual(user.username, 'some-sub')
125 | self.assertEqual(user.first_name, 'Some given name')
126 | self.assertEqual(user.last_name, 'Some family name')
127 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/services/realm/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/services/realm/__init__.py
--------------------------------------------------------------------------------
/src/django_keycloak/tests/services/realm/test_get_realm_api_client.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from django_keycloak.factories import ServerFactory, RealmFactory
4 | from django_keycloak.tests.mixins import MockTestCaseMixin
5 |
6 | import django_keycloak.services.realm
7 |
8 |
9 | class ServicesRealmGetRealmApiClientTestCase(
10 | MockTestCaseMixin, TestCase):
11 |
12 | def setUp(self):
13 | self.server = ServerFactory(
14 | url='https://some-url',
15 | internal_url=''
16 | )
17 |
18 | self.realm = RealmFactory(
19 | server=self.server,
20 | name='test-realm'
21 | )
22 |
23 | def test_get_realm_api_client(self):
24 | """
25 | Case: a realm api client is requested for a realm on a server without
26 | internal_url.
27 | Expected: a KeycloakRealm client is returned with settings based on the
28 | provided realm. The server_url in the client is the provided url.
29 | """
30 | client = django_keycloak.services.realm.\
31 | get_realm_api_client(realm=self.realm)
32 |
33 | self.assertEqual(client.server_url, self.server.url)
34 | self.assertEqual(client.realm_name, self.realm.name)
35 |
36 | def test_get_realm_api_client_with_internal_url(self):
37 | """
38 | Case: a realm api client is requested for a realm on a server with
39 | internal_url.
40 | Expected: a KeycloakRealm client is returned with settings based on the
41 | provided realm. The server_url in the client is the provided url.
42 | """
43 | self.server.internal_url = 'https://some-internal-url'
44 |
45 | client = django_keycloak.services.realm.\
46 | get_realm_api_client(realm=self.realm)
47 |
48 | self.assertEqual(client.server_url, self.server.internal_url)
49 | self.assertEqual(client.realm_name, self.realm.name)
50 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/services/realm/test_refresh_well_known_oidc.py:
--------------------------------------------------------------------------------
1 | import mock
2 |
3 | from django.test import TestCase
4 |
5 | from django_keycloak.factories import RealmFactory
6 | from django_keycloak.tests.mixins import MockTestCaseMixin
7 |
8 | import django_keycloak.services.realm
9 |
10 |
11 | class ServicesRealmRefreshWellKnownOIDCTestCase(
12 | MockTestCaseMixin, TestCase):
13 |
14 | def setUp(self):
15 | self.realm = RealmFactory(
16 | name='test-realm',
17 | _well_known_oidc='empty'
18 | )
19 |
20 | keycloak_oidc_mock = mock.MagicMock()
21 | keycloak_oidc_mock.well_known.contents = {'key': 'value'}
22 | self.setup_mock('keycloak.realm.KeycloakRealm.open_id_connect',
23 | return_value=keycloak_oidc_mock)
24 |
25 | def test_refresh_well_known_oidc(self):
26 | """
27 | Case: An update is requested for the .well-known for a specified realm.
28 | Expected: The .well-known is updated.
29 | """
30 | self.assertEqual(self.realm._well_known_oidc, 'empty')
31 |
32 | django_keycloak.services.realm.refresh_well_known_oidc(
33 | realm=self.realm
34 | )
35 |
36 | self.assertEqual(self.realm._well_known_oidc, '{"key": "value"}')
37 |
--------------------------------------------------------------------------------
/src/django_keycloak/tests/settings.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django_keycloak.app_settings import * # noqa: F403,F401
4 |
5 | PASSWORD_HASHERS = (
6 | 'django.contrib.auth.hashers.MD5PasswordHasher',
7 | )
8 |
9 | # SECURITY WARNING: keep the secret key used in production secret!
10 | SECRET_KEY = 'secret-key'
11 |
12 | # SECURITY WARNING: don't run with debug turned on in production!
13 | DEBUG = False
14 |
15 | LOGIN_URL = 'keycloak_login'
16 | LOGOUT_REDIRECT_URL = 'index'
17 |
18 | # Application definition
19 | INSTALLED_APPS = [
20 | 'django.contrib.auth',
21 | 'django.contrib.contenttypes',
22 |
23 | 'django_keycloak.apps.KeycloakAppConfig',
24 | ]
25 |
26 | MIDDLEWARE = [
27 | 'django_keycloak.middleware.BaseKeycloakMiddleware',
28 | ]
29 |
30 | AUTHENTICATION_BACKENDS = [
31 | 'django.contrib.auth.backends.ModelBackend',
32 | 'django_keycloak.auth.backends.KeycloakAuthorizationCodeBackend',
33 | ]
34 |
35 | DATABASES = {
36 | 'default': {
37 | 'ENGINE': 'django.db.backends.sqlite3',
38 | 'NAME': ':memory:',
39 | }
40 | }
41 |
42 | logging.disable(logging.CRITICAL)
43 |
--------------------------------------------------------------------------------
/src/django_keycloak/urls.py:
--------------------------------------------------------------------------------
1 | """resource_provider URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15 | """
16 | from django.conf.urls import url
17 |
18 | from django_keycloak import views
19 |
20 | urlpatterns = [
21 | url(r'^login$', views.Login.as_view(), name='keycloak_login'),
22 | url(r'^login-complete$', views.LoginComplete.as_view(),
23 | name='keycloak_login_complete'),
24 | url(r'^logout$', views.Logout.as_view(), name='keycloak_logout'),
25 | url(r'^session-iframe', views.SessionIframe.as_view(),
26 | name='keycloak_session_iframe')
27 | ]
28 |
--------------------------------------------------------------------------------
/src/django_keycloak/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import logging
5 |
6 | from django.shortcuts import resolve_url
7 |
8 | from django_keycloak.services.oidc_profile import get_remote_user_model
9 |
10 | try:
11 | from urllib.parse import urljoin # noqa: F401
12 | except ImportError:
13 | from urlparse import urljoin # noqa: F401
14 |
15 | from django.conf import settings
16 | from django.contrib.auth import authenticate, login, logout
17 | from django.http.response import (
18 | HttpResponseBadRequest,
19 | HttpResponseServerError,
20 | HttpResponseRedirect
21 | )
22 | from django.urls.base import reverse
23 | from django.views.generic.base import (
24 | RedirectView,
25 | TemplateView
26 | )
27 |
28 | from django_keycloak.models import Nonce
29 | from django_keycloak.auth import remote_user_login
30 |
31 |
32 | logger = logging.getLogger(__name__)
33 |
34 |
35 | class Login(RedirectView):
36 |
37 | def get_redirect_url(self, *args, **kwargs):
38 |
39 | nonce = Nonce.objects.create(
40 | redirect_uri=self.request.build_absolute_uri(
41 | location=reverse('keycloak_login_complete')),
42 | next_path=self.request.GET.get('next'))
43 |
44 | self.request.session['oidc_state'] = str(nonce.state)
45 |
46 | authorization_url = self.request.realm.client.openid_api_client\
47 | .authorization_url(
48 | redirect_uri=nonce.redirect_uri,
49 | scope='openid given_name family_name email',
50 | state=str(nonce.state)
51 | )
52 |
53 | if self.request.realm.server.internal_url:
54 | authorization_url = authorization_url.replace(
55 | self.request.realm.server.internal_url,
56 | self.request.realm.server.url,
57 | 1
58 | )
59 |
60 | logger.debug(authorization_url)
61 |
62 | return authorization_url
63 |
64 |
65 | class LoginComplete(RedirectView):
66 |
67 | def get(self, *args, **kwargs):
68 | request = self.request
69 |
70 | if 'error' in request.GET:
71 | return HttpResponseServerError(request.GET['error'])
72 |
73 | if 'code' not in request.GET and 'state' not in request.GET:
74 | return HttpResponseBadRequest()
75 |
76 | if 'oidc_state' not in request.session \
77 | or request.GET['state'] != request.session['oidc_state']:
78 | # Missing or incorrect state; login again.
79 | return HttpResponseRedirect(reverse('keycloak_login'))
80 |
81 | nonce = Nonce.objects.get(state=request.GET['state'])
82 |
83 | user = authenticate(request=request,
84 | code=request.GET['code'],
85 | redirect_uri=nonce.redirect_uri)
86 |
87 | RemoteUserModel = get_remote_user_model()
88 | if isinstance(user, RemoteUserModel):
89 | remote_user_login(request, user)
90 | else:
91 | login(request, user)
92 |
93 | nonce.delete()
94 |
95 | return HttpResponseRedirect(nonce.next_path or '/')
96 |
97 |
98 | class Logout(RedirectView):
99 |
100 | def get_redirect_url(self, *args, **kwargs):
101 | if hasattr(self.request.user, 'oidc_profile'):
102 | self.request.realm.client.openid_api_client.logout(
103 | self.request.user.oidc_profile.refresh_token
104 | )
105 | self.request.user.oidc_profile.access_token = None
106 | self.request.user.oidc_profile.expires_before = None
107 | self.request.user.oidc_profile.refresh_token = None
108 | self.request.user.oidc_profile.refresh_expires_before = None
109 | self.request.user.oidc_profile.save(update_fields=[
110 | 'access_token',
111 | 'expires_before',
112 | 'refresh_token',
113 | 'refresh_expires_before'
114 | ])
115 |
116 | logout(self.request)
117 |
118 | if settings.LOGOUT_REDIRECT_URL:
119 | return resolve_url(settings.LOGOUT_REDIRECT_URL)
120 |
121 | return reverse('keycloak_login')
122 |
123 |
124 | class SessionIframe(TemplateView):
125 | template_name = 'django_keycloak/session_iframe.html'
126 |
127 | @property
128 | def op_location(self):
129 | realm = self.request.realm
130 | if realm.server.internal_url:
131 | return realm.well_known_oidc['check_session_iframe'].replace(
132 | realm.server.internal_url,
133 | realm.server.url,
134 | 1
135 | )
136 | return realm.server.url
137 |
138 | @property
139 | def client_id(self):
140 | if not hasattr(self.request, 'realm'):
141 | return None
142 |
143 | realm = self.request.realm
144 | return realm.client.client_id
145 |
146 | def get_context_data(self, **kwargs):
147 | return super(SessionIframe, self).get_context_data(
148 | client_id=self.client_id,
149 | identity_server=self.request.realm.server.url,
150 | op_location=self.op_location,
151 | cookie_name=getattr(settings, 'KEYCLOAK_SESSION_STATE_COOKIE_NAME',
152 | 'session_state')
153 | )
154 |
--------------------------------------------------------------------------------