├── .github
├── release-drafter.yml
└── workflows
│ ├── pypi.yml
│ ├── python-app.yml
│ └── release-drafter.yml
├── .gitignore
├── .isort.cfg
├── .readthedocs.yml
├── LICENSE
├── README.md
├── docs
├── Makefile
├── make.bat
└── source
│ ├── _images
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ └── oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png
│ ├── _static
│ ├── custom.css
│ └── logo.png
│ ├── _templates
│ └── idpy_template
│ │ ├── footer.html
│ │ └── layout.html
│ ├── conf.py
│ ├── contents
│ ├── clients.rst
│ ├── conf.rst
│ ├── developers.md
│ ├── faq.md
│ ├── intro.rst
│ ├── session_management.rst
│ ├── setup.md
│ └── usage.md
│ ├── diagrams
│ └── session_relations.mermaid
│ └── index.rst
├── example
├── django_op
│ └── README.md
├── fastapi
│ ├── __init__.py
│ ├── config.json
│ ├── main.py
│ ├── models.py
│ ├── passwd.json
│ ├── users.json
│ └── utils.py
└── flask_op
│ ├── Dockerfile
│ ├── README.md
│ ├── __init__.py
│ ├── application.py
│ ├── certs
│ ├── cert.pem
│ ├── client.crt
│ ├── client.key
│ └── key.pem
│ ├── config.json
│ ├── passwd.json
│ ├── requirements.txt
│ ├── run.sh
│ ├── server.py
│ ├── templates
│ ├── check_session_iframe.html
│ ├── error.html
│ ├── frontchannel_logout.html
│ ├── index.html
│ ├── logout.html
│ ├── post_logout.html
│ └── user_pass.jinja2
│ ├── users.json
│ ├── views.py
│ └── yaml_to_json.py
├── pyproject.toml
├── requirements-dev.txt
├── requirements-docs.txt
├── requirements.txt
├── setup.py
├── src
└── oidcop
│ ├── __init__.py
│ ├── authn_event.py
│ ├── authz
│ └── __init__.py
│ ├── client_authn.py
│ ├── configure.py
│ ├── constant.py
│ ├── construct.py
│ ├── cookie_handler.py
│ ├── endpoint.py
│ ├── endpoint_context.py
│ ├── exception.py
│ ├── logging.py
│ ├── login_hint.py
│ ├── oauth2
│ ├── __init__.py
│ ├── add_on
│ │ ├── __init__.py
│ │ ├── dpop.py
│ │ └── extra_args.py
│ ├── authorization.py
│ ├── introspection.py
│ ├── pushed_authorization.py
│ └── token.py
│ ├── oidc
│ ├── __init__.py
│ ├── add_on
│ │ ├── __init__.py
│ │ ├── custom_scopes.py
│ │ └── pkce.py
│ ├── authorization.py
│ ├── discovery.py
│ ├── provider_config.py
│ ├── read_registration.py
│ ├── registration.py
│ ├── session.py
│ ├── token.py
│ └── userinfo.py
│ ├── scopes.py
│ ├── server.py
│ ├── session
│ ├── __init__.py
│ ├── claims.py
│ ├── database.py
│ ├── grant.py
│ ├── info.py
│ ├── manager.py
│ └── token.py
│ ├── template_handler.py
│ ├── token
│ ├── __init__.py
│ ├── exception.py
│ ├── handler.py
│ ├── id_token.py
│ └── jwt_token.py
│ ├── user_authn
│ ├── __init__.py
│ ├── authn_context.py
│ └── user.py
│ ├── user_info
│ └── __init__.py
│ ├── util.py
│ └── utils.py
└── tests
├── __init__.py
├── donot_test_49_session_persistence.py
├── logging.yaml
├── logging_config.json
├── op_config.json
├── op_config_defaults.py
├── passwd.json
├── srv_config.yaml
├── templates
└── user_pass.jinja2
├── test_00_configure.py
├── test_00_server.py
├── test_01_claims.py
├── test_01_grant.py
├── test_01_session_info.py
├── test_01_session_token.py
├── test_01_util.py
├── test_02_authz_handling.py
├── test_02_client_authn.py
├── test_02_sess_mngm_db.py
├── test_04_token_handler.py
├── test_05_id_token.py
├── test_05_jwt_token.py
├── test_06_authn_context.py
├── test_06_session_manager.py
├── test_06_session_manager_pairwise.py
├── test_07_userinfo.py
├── test_08_session_life.py
├── test_09_cookie_handler.py
├── test_12_user_authn.py
├── test_13_login_hint.py
├── test_20_endpoint.py
├── test_21_oidc_discovery_endpoint.py
├── test_22_oidc_provider_config_endpoint.py
├── test_23_oidc_registration_endpoint.py
├── test_24_oauth2_authorization_endpoint.py
├── test_24_oauth2_authorization_endpoint_jar.py
├── test_24_oauth2_token_endpoint.py
├── test_24_oidc_authorization_endpoint.py
├── test_26_oidc_userinfo_endpoint.py
├── test_30_oidc_end_session.py
├── test_31_oauth2_introspection.py
├── test_32_oidc_read_registration.py
├── test_33_oauth2_pkce.py
├── test_34_oidc_sso.py
├── test_35_oidc_token_endpoint.py
├── test_36_oauth2_token_exchange.py
├── test_40_oauth2_pushed_authorization.py
├── test_50_persistence.py
├── test_60_dpop.py
├── test_61_add_on.py
└── users.json
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | categories:
4 | -
5 | title: 'Features'
6 | labels:
7 | - 'enhancement'
8 | - 'feat'
9 | - 'feature'
10 | -
11 | title: 'Bug Fixes'
12 | labels:
13 | - 'bug'
14 | - 'bugfix'
15 | - 'fix'
16 | -
17 | title: 'Maintenance'
18 | labels:
19 | - 'chore'
20 | - 'style'
21 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
22 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
23 | version-resolver:
24 | major:
25 | labels: ['major']
26 | minor:
27 | labels: ['minor']
28 | patch:
29 | labels: ['patch']
30 | default: patch
31 | exclude-labels: ['skip']
32 | autolabeler:
33 | -
34 | label: 'bug'
35 | branch:
36 | - '/bug\/.+/'
37 | - '/bugfix\/.+/'
38 | - '/fix\/.+/'
39 | -
40 | label: 'enhancement'
41 | branch:
42 | - '/dependabot\/.+/'
43 | - '/enhancement\/.+/'
44 | - '/feat\/.+/'
45 | - '/feature\/.+/'
46 | -
47 | label: 'chore'
48 | branch:
49 | - '/chore\/.+/'
50 | - '/style\/.+/'
51 | template: |
52 | ## Release notes
53 |
54 | $CHANGES
55 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python distribution to PyPI
2 | on:
3 | release:
4 | types:
5 | - published
6 |
7 | jobs:
8 | build-n-publish:
9 | name: Publish Python distribution to PyPI
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@master
13 | - name: Setup Python 3.8
14 | uses: actions/setup-python@v1
15 | with:
16 | python-version: 3.8
17 | - name: Install pypa/build
18 | run: >-
19 | python -m
20 | pip install
21 | build
22 | --user
23 | - name: Build a binary wheel and a source tarball
24 | run: >-
25 | python -m
26 | build
27 | --sdist
28 | --wheel
29 | --outdir dist/
30 | .
31 | - name: Publish distribution to PyPI
32 | uses: pypa/gh-action-pypi-publish@master
33 | with:
34 | user: __token__
35 | password: ${{ secrets.PYPI_API_TOKEN }}
36 |
--------------------------------------------------------------------------------
/.github/workflows/python-app.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: oidc-op
5 |
6 | on:
7 | push:
8 | branches: [ master, develop ]
9 | pull_request:
10 | branches: [ master, develop ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | python-version:
21 | - '3.7'
22 | - '3.8'
23 | - '3.9'
24 |
25 | steps:
26 | - uses: actions/checkout@v2
27 | - name: Set up Python ${{ matrix.python-version }}
28 | uses: actions/setup-python@v2
29 | with:
30 | python-version: ${{ matrix.python-version }}
31 | - name: Install dependencies
32 | run: |
33 | python -m pip install --upgrade pip
34 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
35 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
36 | python setup.py install
37 | - name: Lint with flake8
38 | run: |
39 | # stop the build if there are Python syntax errors or undefined names
40 | flake8 src/oidcop --count --select=E9,F63,F7,F82 --show-source --statistics
41 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
42 | flake8 src/oidcop --max-line-length 120 --count --exit-zero --statistics
43 |
44 | - name: Test with pytest
45 | run: |
46 | pytest --cov=oidcop tests/
47 | - name: Bandit Security Scan
48 | run: |
49 | bandit --skip B105,B106,B107 -r src/oidcop/
50 | #- name: Upload coverage to Codecov
51 | #uses: codecov/codecov-action@v1
52 | #with:
53 | #token: ${{ secrets.CODECOV_TOKEN }}
54 | #file: example/coverage.xml
55 | #flags: unittests
56 | #env_vars: OS,PYTHON
57 | #name: codecov-umbrella
58 | #fail_ci_if_error: true
59 | #path_to_write_report: ./codecov_report.txt
60 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release drafter
2 |
3 | on:
4 | push:
5 | branches: [master, develop]
6 | pull_request:
7 | types: [opened, reopened, synchronize]
8 |
9 | jobs:
10 | update_release_draft:
11 | name: Update draft release
12 | runs-on: ubuntu-latest
13 | steps:
14 | -
15 | uses: release-drafter/release-drafter@v5
16 | env:
17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | static/
2 | private/
3 | conf.yaml
4 | flask_op/debug.log
5 | flask_op/static/
6 | debug.log
7 | .pytest_cache/
8 | # Created by .ignore support plugin (hsz.mobi)
9 | ### Python template
10 | # Byte-compiled / optimized / DLL files
11 | __pycache__/
12 | *.py[cod]
13 | *$py.class
14 |
15 | # C extensions
16 | *.so
17 |
18 | # Distribution / packaging
19 | .Python
20 | build/
21 | develop-eggs/
22 | dist/
23 | downloads/
24 | eggs/
25 | .eggs/
26 | lib/
27 | lib64/
28 | parts/
29 | sdist/
30 | var/
31 | wheels/
32 | *.egg-info/
33 | .installed.cfg
34 | *.egg
35 | MANIFEST
36 |
37 | # PyInstaller
38 | # Usually these files are written by a python script from a template
39 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
40 | *.manifest
41 | *.spec
42 |
43 | # Installer logs
44 | pip-log.txt
45 | pip-delete-this-directory.txt
46 |
47 | # Unit test / coverage reports
48 | htmlcov/
49 | .tox/
50 | .coverage
51 | .coverage.*
52 | .cache
53 | nosetests.xml
54 | coverage.xml
55 | *.cover
56 | .hypothesis/
57 |
58 | # Translations
59 | *.mo
60 | *.pot
61 |
62 | # Django stuff:
63 | *.log
64 | .static_storage/
65 | .media/
66 | local_settings.py
67 |
68 | # Flask stuff:
69 | instance/
70 | .webassets-cache
71 |
72 | # Scrapy stuff:
73 | .scrapy
74 |
75 | # Sphinx documentation
76 | docs/_build/
77 |
78 | # PyBuilder
79 | target/
80 |
81 | # Jupyter Notebook
82 | .ipynb_checkpoints
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # celery beat schedule file
88 | celerybeat-schedule
89 |
90 | # SageMath parsed files
91 | *.sage.py
92 |
93 | # Environments
94 | .env
95 | .venv
96 | env/
97 | venv/
98 | ENV/
99 | env.bak/
100 | venv.bak/
101 |
102 | # Spyder project settings
103 | .spyderproject
104 | .spyproject
105 |
106 | # Rope project settings
107 | .ropeproject
108 |
109 | # mkdocs documentation
110 | /site
111 |
112 | # mypy
113 | .mypy_cache/
114 |
115 | .idea/
116 | src/oidcendpoint.egg-info/
117 |
118 | .iframes/
119 | tests/pairwise.salt
120 | tests/public.salt
121 |
--------------------------------------------------------------------------------
/.isort.cfg:
--------------------------------------------------------------------------------
1 | [settings]
2 | force_single_line = 1
3 | known_first_party = oidcop
4 | known_third_party = cryptojwt, oidcmsg
5 | known_future_library = future,past
6 | default_section = THIRDPARTY
7 | line_length = 100
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Build documentation in the docs/ directory with Sphinx
9 | sphinx:
10 | configuration: docs/source/conf.py
11 |
12 | # Build documentation with MkDocs
13 | #mkdocs:
14 | # configuration: mkdocs.yml
15 |
16 | # Optionally build your docs in additional formats such as PDF and ePub
17 | formats: all
18 |
19 | # Optionally set the version of Python and requirements required to build your docs
20 | python:
21 | version: 3.8
22 | install:
23 | - requirements: requirements-docs.txt
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # oidc-op
2 |
3 | 
4 | 
5 | [](https://pepy.tech/project/oidcop)
6 | [](https://pepy.tech/project/oidcop)
7 | 
8 | 
9 | 
10 |
11 | This project is a Python implementation of an **OIDC Provider** on top of [jwtconnect.io](https://jwtconnect.io/) that shows to you how to 'build' an OP using the classes and functions provided by oidc-op.
12 |
13 | If you want to add or replace functionality the official documentation should be able to tell you how.
14 | If you are just going to build a standard OP you only have to understand how to write your configuration file.
15 | In `example/` folder you'll find some complete examples based on flask and django.
16 |
17 | Idpy OIDC-op implements the following standards:
18 |
19 | * [OpenID Connect Core 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-core-1_0.html)
20 | * [Web Finger](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery)
21 | * [OpenID Connect Discovery 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-discovery-1_0.html)
22 | * [OpenID Connect Dynamic Client Registration 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-registration-1_0.html)
23 | * [OpenID Connect Session Management 1.0](https://openid.net/specs/openid-connect-session-1_0.html)
24 | * [OpenID Connect Back-Channel Logout 1.0](https://openid.net/specs/openid-connect-backchannel-1_0.html)
25 | * [OpenID Connect Front-Channel Logout 1.0](https://openid.net/specs/openid-connect-frontchannel-1_0.html)
26 | * [OAuth2 Token introspection](https://tools.ietf.org/html/rfc7662)
27 |
28 | It also comes with the following `add_on` modules.
29 |
30 | * Custom scopes, that extends [OIDC standard ScopeClaims](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims)
31 | * [Proof Key for Code Exchange by OAuth Public Clients (PKCE)](https://tools.ietf.org/html/rfc7636)
32 | * [OAuth2 PAR](https://datatracker.ietf.org/doc/html/rfc9126)
33 | * [OAuth2 RAR](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar)
34 | * [OAuth2 DPoP](https://tools.ietf.org/id/draft-fett-oauth-dpop-04.html)
35 | * [OAuth 2.0 Authorization Server Issuer Identification](https://datatracker.ietf.org/doc/draft-ietf-oauth-iss-auth-resp)
36 |
37 | The entire project code is open sourced and therefore licensed under the [Apache 2.0](https://en.wikipedia.org/wiki/Apache_License)
38 |
39 | For any futher information please read the [Official Documentation](https://oidcop.readthedocs.io/en/latest/).
40 |
41 | # Certifications
42 | [](https://www.certification.openid.net/plan-detail.html?public=true&plan=7p3iPQmff6Ohv)
43 |
44 |
45 | # Contribute
46 |
47 | [Join in](https://idpy.org/contribute/).
48 |
49 |
50 | # Authors
51 |
52 | - Roland Hedberg
53 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
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)
21 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/_images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IdentityPython/oidc-op/2f81e246ff9ef412f5dc786cd2cfa5dd5fccf9e1/docs/source/_images/1.png
--------------------------------------------------------------------------------
/docs/source/_images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IdentityPython/oidc-op/2f81e246ff9ef412f5dc786cd2cfa5dd5fccf9e1/docs/source/_images/2.png
--------------------------------------------------------------------------------
/docs/source/_images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IdentityPython/oidc-op/2f81e246ff9ef412f5dc786cd2cfa5dd5fccf9e1/docs/source/_images/3.png
--------------------------------------------------------------------------------
/docs/source/_images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IdentityPython/oidc-op/2f81e246ff9ef412f5dc786cd2cfa5dd5fccf9e1/docs/source/_images/4.png
--------------------------------------------------------------------------------
/docs/source/_images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IdentityPython/oidc-op/2f81e246ff9ef412f5dc786cd2cfa5dd5fccf9e1/docs/source/_images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png
--------------------------------------------------------------------------------
/docs/source/_static/custom.css:
--------------------------------------------------------------------------------
1 | body,
2 | h1, h2,
3 | .rst-content .toctree-wrapper p.caption,
4 | h3, h4, h5, h6,
5 | legend{
6 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
7 | }
8 |
9 | .wy-side-nav-search{
10 | background: #ffffff;
11 | }
12 |
13 | .wy-side-nav-search>a,
14 | .wy-side-nav-search .wy-dropdown>a{
15 | color: #9b9c9e;
16 | font-weight: normal;
17 | }
18 |
19 | .wy-menu-vertical header,
20 | .wy-menu-vertical p.caption{
21 | color: #fff;
22 | font-size:85%;
23 | }
24 |
25 | .wy-nav-top{
26 | background: #fff;
27 | border-bottom: 1px solid #f7f5f5;
28 | }
29 |
30 | .wy-nav-top a{
31 | display: block;
32 | color: #9b9c9e;
33 | font-weight: normal;
34 | }
35 |
36 | .wy-nav-top i{
37 | color: #BE0417;
38 | }
39 |
40 | .wy-nav-top img{
41 | border-radius: 0;
42 | background: none;
43 | width: 65%;
44 | }
45 |
46 | img{
47 | height: auto !important;
48 | }
49 |
50 | .document{
51 | text-align: justify;
52 | }
53 |
54 | h1{
55 | text-align: left;
56 | }
57 |
58 | #logo_main{
59 | margin-bottom: 0;
60 | }
61 |
62 | #title_under_logo{
63 | margin-bottom: 1em;
64 | }
65 |
66 | .alert-danger {
67 | color: #721c24;
68 | background-color: #f8d7da;
69 | border-color: #f5c6cb;
70 | }
71 | .alert-primary {
72 | color: #004085;
73 | background-color: #cce5ff;
74 | border-color: #b8daff;
75 | }
76 | .alert {
77 | position: relative;
78 | padding: .75rem 1.25rem;
79 | margin-bottom: 1rem;
80 | border: 1px solid transparent;
81 | border-radius: .25rem;
82 | }
83 |
--------------------------------------------------------------------------------
/docs/source/_static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IdentityPython/oidc-op/2f81e246ff9ef412f5dc786cd2cfa5dd5fccf9e1/docs/source/_static/logo.png
--------------------------------------------------------------------------------
/docs/source/_templates/idpy_template/footer.html:
--------------------------------------------------------------------------------
1 |
54 |
--------------------------------------------------------------------------------
/docs/source/_templates/idpy_template/layout.html:
--------------------------------------------------------------------------------
1 | {% extends "!layout.html" %}
2 |
3 | {% block extrahead %}
4 |
5 |
6 | {% endblock %}
7 |
8 |
9 |
10 | {% block sidebartitle %}
11 |
12 | {% if logo %}
13 | {# Not strictly valid HTML, but it's the only way to display/scale
14 | it properly, without weird scripting or heaps of work
15 | #}
16 |
17 | {% endif %}
18 |
19 | {% if logo and theme_logo_only %}
20 |
21 | {% else %}
22 | {{ project }}
23 | {% endif %}
24 |
25 |
26 | {% if theme_display_version %}
27 | {%- set nav_version = version %}
28 | {% if READTHEDOCS and current_version %}
29 | {%- set nav_version = current_version %}
30 | {% endif %}
31 | {% if nav_version %}
32 |
33 | {{ nav_version }}
34 |
35 | {% endif %}
36 | {% endif %}
37 |
38 | {% include "searchbox.html" %}
39 |
40 | {% endblock %}
41 |
42 |
43 |
44 |
45 | {% block mobile_nav %}
46 |
47 |
48 |
49 |
50 |
51 | {{ project }}
52 |
53 | {% endblock %}
54 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | # import os
14 | # import sys
15 | # sys.path.insert(0, os.path.abspath('.'))
16 | # from recommonmark.parser import CommonMarkParser
17 |
18 | # -- Project information -----------------------------------------------------
19 |
20 | project = 'oidcop'
21 | copyright = '2021, Identity Python'
22 | author = 'Giuseppe De Marco, Roland Hedberg'
23 |
24 | # The full version, including alpha/beta/rc tags
25 | release = '0.1.0'
26 |
27 |
28 | # -- General configuration ---------------------------------------------------
29 |
30 | # Add any Sphinx extension module names here, as strings. They can be
31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32 | # ones.
33 | extensions = ['sphinxcontrib.images', 'recommonmark']
34 |
35 | # Add any paths that contain templates here, relative to this directory.
36 | templates_path = ['_templates/idpy_template']
37 | html_logo = "_static/logo.png"
38 |
39 | # List of patterns, relative to source directory, that match files and
40 | # directories to ignore when looking for source files.
41 | # This pattern also affects html_static_path and html_extra_path.
42 | exclude_patterns = []
43 |
44 |
45 | # -- Options for HTML output -------------------------------------------------
46 |
47 | # The theme to use for HTML and HTML Help pages. See the documentation for
48 | # a list of builtin themes.
49 | #
50 | html_theme = 'sphinx_rtd_theme'
51 | # Add any paths that contain custom static files (such as style sheets) here,
52 | # relative to this directory. They are copied after the builtin static files,
53 | # so a file named "default.css" will overwrite the builtin "default.css".
54 | html_static_path = ['_static']
55 |
56 | source_suffix = ['.rst', '.md']
57 |
--------------------------------------------------------------------------------
/docs/source/contents/clients.rst:
--------------------------------------------------------------------------------
1 | ********************
2 | The clients database
3 | ********************
4 |
5 | Information kept about clients in the client database are to begin with the
6 | client metadata as defined in
7 | https://openid.net/specs/openid-connect-registration-1_0.html .
8 |
9 | To that we have the following additions specified in OIDC extensions.
10 |
11 | * https://openid.net/specs/openid-connect-rpinitiated-1_0.html
12 | + post_logout_redirect_uri
13 | * https://openid.net/specs/openid-connect-frontchannel-1_0.html
14 | + frontchannel_logout_uri
15 | + frontchannel_logout_session_required
16 | * https://openid.net/specs/openid-connect-backchannel-1_0.html#Backchannel
17 | + backchannel_logout_uri
18 | + backchannel_logout_session_required
19 | * https://openid.net/specs/openid-connect-federation-1_0.html#rfc.section.3.1
20 | + client_registration_types
21 | + organization_name
22 | + signed_jwks_uri
23 |
24 | And finally we add a number of parameters that are OidcOP specific.
25 | These are described in this document.
26 |
27 | --------------
28 | allowed_scopes
29 | --------------
30 |
31 | Which scopes that can be returned to a client. This is used to filter
32 | the set of scopes a user can authorize release of.
33 |
34 | -----------------
35 | token_usage_rules
36 | -----------------
37 |
38 | There are usage rules for tokens. Rules are set per token type (the basic set is
39 | authorization_code, refresh_token, access_token and id_token).
40 | The possible rules are:
41 |
42 | + how many times they can be used
43 | + if other tokens can be minted based on this token
44 | + how fast they expire
45 |
46 | A typical example (this is the default) would be::
47 |
48 | "token_usage_rules": {
49 | "authorization_code": {
50 | "max_usage": 1
51 | "supports_minting": ["access_token", "refresh_token"],
52 | "expires_in": 600,
53 | },
54 | "refresh_token": {
55 | "supports_minting": ["access_token"],
56 | "expires_in": -1
57 | },
58 | }
59 |
60 | This then means that access_tokens can be used any number of times,
61 | can not be used to mint other tokens and will expire after 300 seconds
62 | which is the default for any token. An authorization_code can only used once
63 | and it can be used to mint access_tokens and refresh_tokens. Note that normally
64 | an authorization_code is used to mint an access_token and a refresh_token at
65 | the same time. Such a dual minting is counted as one usage.
66 | And lastly an refresh_token can be used to mint access_tokens any number of
67 | times. An *expires_in* of -1 means that the token will never expire.
68 |
69 | If token_usage_rules are defined in the client metadata then it will be used
70 | whenever a token is minted unless circumstances makes the OP modify the rules.
71 |
72 | Also this does not mean that what is valid for a token can not be changed
73 | during run time.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/docs/source/contents/developers.md:
--------------------------------------------------------------------------------
1 | Tests
2 | -----
3 |
4 | ````
5 | pip install -r requirements-dev.txt
6 | pytest --cov=oidcop tests/
7 | ````
8 |
--------------------------------------------------------------------------------
/docs/source/contents/faq.md:
--------------------------------------------------------------------------------
1 | FAQ
2 | -----
3 |
4 | *
5 |
--------------------------------------------------------------------------------
/docs/source/contents/intro.rst:
--------------------------------------------------------------------------------
1 | ***************************
2 | The OpenID Connect Provider
3 | ***************************
4 |
5 | ============
6 | Introduction
7 | ============
8 |
9 | This documentation are here to show you how to 'build' an OP using the
10 | classes and functions provided by oidcop.
11 |
12 | OAuth2 and thereby OpenID Connect (OIDC) are built on a request-response paradigm.
13 | The RP issues a request and the OP returns a response.
14 |
15 | The OIDC core standard defines a set of such request-responses.
16 | This is a basic list of request-responses and the normal sequence in which they
17 | occur:
18 |
19 | 1. Provider discovery (WebFinger)
20 | 2. Provider Info Discovery
21 | 3. Client registration
22 | 4. Authorization/Authentication
23 | 5. Access token
24 | 6. User info
25 |
26 | If you are just going to build a standard OP you only have to write the
27 | configuration file and of course add authentication and user consent services.
28 | If you want to add or replace functionality this document should be able to
29 | tell you how.
30 |
31 | Setting up an OP means making a number if decisions. Like, should the OP support
32 | WebFinger_ , `dynamic discovery`_ and/or `dynamic client registration`_ .
33 |
34 | All these are services you can access at endpoints. The total set of endpoints
35 | that this package supports are
36 |
37 | - webfinger
38 | - provider_info
39 | - registration
40 | - authorization
41 | - token
42 | - refresh_token
43 | - userinfo
44 | - end_session
45 |
46 | .. _WebFinger: https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery
47 | .. _dynamic discovery: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
48 | .. _dynamic client registration: https://openid.net/specs/openid-connect-registration-1_0.html
49 |
50 | ===============
51 | Endpoint layout
52 | ===============
53 |
54 | When an endpoint receives a request it has to do a number of things:
55 |
56 | - Verify that the client can issue the request (client authentication/authorization)
57 | - Verify that the request is correct and that it contains the necessary information.
58 | - Process the request, which includes applying server policies and gathering information.
59 | - Construct the response
60 |
61 | I should note at this point that this package is expected to work within the
62 | confines of a web server framework such that the actual receiving and sending
63 | of the HTTP messages are dealt with by the framework.
64 |
65 | Based on the actions an endpoint has to perform a method call structure
66 | has been constructed. It looks like this:
67 |
68 | 1. parse_request
69 |
70 | - client_authentication (*)
71 | - post_parse_request (*)
72 |
73 | 2. process_request
74 |
75 | 3. do_response
76 |
77 | - response_info
78 | - construct
79 | - pre_construct (*)
80 | - _parse_args
81 | - post_construct (*)
82 | - update_http_args
83 |
84 | Steps marked with '*' are places where extensions can be applied.
85 |
86 | *parse_request* expects as input the request itself in a number of formats and
87 | also, if available, information about client authentication. The later is
88 | normally the authorization element of the HTTP header.
89 |
90 | *do_response* returns a dictionary that can look like this::
91 |
92 | {
93 | 'response':
94 | _response as a string or as a Message instance_
95 | 'http_headers': [
96 | ('Content-type', 'application/json'),
97 | ('Pragma', 'no-cache'),
98 | ('Cache-Control', 'no-store')
99 | ],
100 | 'cookie': _list of cookies_,
101 | 'response_placement': 'body'
102 | }
103 |
104 | cookie
105 | MAY be present
106 | http_headers
107 | MAY be present
108 | http_response
109 | Already clear and formatted HTTP response
110 | response
111 | MUST be present
112 | response_placement
113 | If absent defaults to the endpoints response_placement parameter value or
114 | if that is also missing 'url'
115 | redirect_location
116 | Where to send a redirect
117 |
--------------------------------------------------------------------------------
/docs/source/contents/setup.md:
--------------------------------------------------------------------------------
1 | Setup
2 | -----
3 |
4 | Create an environment
5 |
6 | virtualenv -ppython3 env
7 | source env/bin/activate
8 |
9 | Install
10 |
11 | pip install oidcop
12 |
13 | Get the usage examples
14 |
15 | git clone https://github.com/identitypython/oidc-op.git
16 | cd oidc-op/example/flask_op/
17 | bash run.sh
18 |
19 |
20 | To configure a standard OIDC Provider you have to edit the oidcop configuration file.
21 | See `example/flask_op/config.json` to get in.
22 |
23 | ~/DEV/IdentityPython/OIDC/oidc-op/example/flask_op$ bash run.sh
24 | 2021-05-02 14:57:44,727 root DEBUG Configured logging using dictionary
25 | 2021-05-02 14:57:44,728 oidcop.configure DEBUG Set server password to {'kty': 'oct', 'use': 'sig', 'k': 'n4G9OjOixYMOotXvP15grwq0peN2zq9I'}
26 | * Serving Flask app "oidc_op" (lazy loading)
27 | * Environment: production
28 | WARNING: This is a development server. Do not use it in a production deployment.
29 | Use a production WSGI server instead.
30 | * Debug mode: on
31 | 2021-05-02 14:57:44,764 werkzeug INFO * Running on https://127.0.0.1:5000/ (Press CTRL+C to quit)
32 | 2021-05-02 14:57:44,765 werkzeug INFO * Restarting with stat
33 | 2021-05-02 14:57:45,011 root DEBUG Configured logging using dictionary
34 | 2021-05-02 14:57:45,011 oidcop.configure DEBUG Set server password to {'kty': 'oct', 'use': 'sig', 'k': 'bceYal7bK9zvlBAA7-23lsi5crcv_8Cd'}
35 | 2021-05-02 14:57:45,037 werkzeug WARNING * Debugger is active!
36 | 2021-05-02 14:57:45,092 werkzeug INFO * Debugger PIN: 560-973-597
37 |
38 |
39 | Then open your browser to `https://127.0.0.1:5000/.well-known/openid-configuration` to get the OpenID Provider Configuration resource.
40 |
41 |
42 | --------------------
43 | JWK Set (JWKS) files
44 | --------------------
45 | see: [cryptojwt documentation](https://cryptojwt.readthedocs.io/en/latest/keyhandling.html private/cookie_sign_jwk.json
101 |
--------------------------------------------------------------------------------
/docs/source/contents/usage.md:
--------------------------------------------------------------------------------
1 | Usage
2 | -----
3 |
4 | Some examples, how to run [flask_op](https://github.com/IdentityPython/oidc-op/tree/master/example/flask_op) and [django_op](https://github.com/peppelinux/django-oidc-op) but also some typical configuration in relation to common use cases.
5 |
6 |
7 |
8 | Configure flask-rp
9 | ------------------
10 |
11 | _JWTConnect-Python-OidcRP_ is Relaing Party for tests, see [related page](https://github.com/openid/JWTConnect-Python-OidcRP).
12 | You can run a working instance of `JWTConnect-Python-OidcRP.flask_rp` with:
13 |
14 | ````
15 | pip install git+https://github.com/openid/JWTConnect-Python-OidcRP.git
16 |
17 | # get entire project to have examples files
18 | git clone https://github.com/openid/JWTConnect-Python-OidcRP.git
19 | cd JWTConnect-Python-OidcRP/example/flask_rp
20 |
21 | # run it as it come
22 | bash run.sh
23 | ````
24 |
25 | Now you can connect to `https://127.0.0.1:8090/` to see the RP landing page and select your authentication endpoint.
26 |
27 | ### Authentication examples
28 |
29 | 
30 |
31 | Get to the RP landing page to choose your authentication endpoint. The first option aims to use _Provider Discovery_.
32 |
33 | ----------------------------------
34 |
35 | 
36 |
37 | The AS/OP supports dynamic client registration, it accepts the authentication request and prompt to us the login form. Read [passwd.json](https://github.com/IdentityPython/oidc-op/blob/master/example/flask_op/passwd.json) file to get credentials.
38 |
39 | ----------------------------------
40 |
41 | 
42 |
43 | The identity representation with the information fetched from the user info endpoint.
44 |
45 | ----------------------------------
46 |
47 | 
48 |
49 | We can even test the single logout
50 |
51 |
52 | Refresh token
53 | -------------
54 |
55 | To obtain a refresh token, you have to use `response_type=code`, add `offline_access` to `scope` and also use `prompt=consent`, otherwise there will be an error (based on [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.11)).
56 |
57 | To refresh a token:
58 |
59 |
60 | import requests
61 |
62 | CLIENT_ID = "DBP60x3KUQfCYWZlqFaS_Q"
63 | CLIENT_SECRET="8526270403788522b2444e87ea90c53bcafb984119cec92eeccc12f1"
64 | REFRESH_TOKEN = "Z0FBQUFBQ ... lN2JNODYtZThjMnFsZUNDcg=="
65 |
66 | data = {
67 | "grant_type" : "refresh_token",
68 | "client_id" : f"{CLIENT_ID}",
69 | "client_secret" : f"{CLIENT_SECRET}",
70 | "refresh_token" : f"{REFRESH_TOKEN}"
71 | }
72 | headers = {'Content-Type': "application/x-www-form-urlencoded" }
73 | response = requests.post(
74 | 'https://127.0.0.1:8000/oidcop/token', verify=False, data=data, headers=headers
75 | )
76 |
77 | oidc-op will return a json response like this:
78 |
79 | {
80 | 'access_token': 'eyJhbGc ... CIOH_09tT_YVa_gyTqg',
81 | 'token_type': 'Bearer',
82 | 'scope': 'openid profile email address phone offline_access',
83 | 'refresh_token': 'Z0FBQ ... 1TE16cm1Tdg=='
84 | }
85 |
86 |
87 |
88 | Introspection endpoint
89 | ----------------------
90 |
91 | Here an example about how to consume oidc-op introspection endpoint.
92 | This example uses a client with an HTTP Basic Authentication::
93 |
94 | import base64
95 | import requests
96 |
97 | TOKEN = "eyJhbGciOiJFUzI1NiIsImtpZCI6IlQwZGZTM1ZVYUcxS1ZubG9VVTQwUXpJMlMyMHpjSHBRYlMxdGIzZ3hZVWhCYzNGaFZWTlpTbWhMTUEifQ.eyJzY29wZSI6IFsib3BlbmlkIiwgInByb2ZpbGUiLCAiZW1haWwiLCAiYWRkcmVzcyIsICJwaG9uZSJdLCAiYXVkIjogWyJvTHlSajdzSkozWHZBWWplRENlOHJRIl0sICJqdGkiOiAiOWQzMjkzYjZiYmNjMTFlYmEzMmU5ODU0MWIwNzE1ZWQiLCAiY2xpZW50X2lkIjogIm9MeVJqN3NKSjNYdkFZamVEQ2U4clEiLCAic3ViIjogIm9MeVJqN3NKSjNYdkFZamVEQ2U4clEiLCAic2lkIjogIlowRkJRVUZCUW1keGJIVlpkRVJKYkZaUFkxQldaa0pQVUVGc1pHOUtWWFZ3VFdkZmVEY3diMVprYmpSamRrNXRMVzB4YTNnelExOHlRbHBHYTNRNVRHZEdUUzF1UW1sMlkzVnhjRE5sUm01dFRFSmxabGRXYVhJeFpFdHVSV2xtUzBKcExWTmFaRzV3VjJodU0yNXlSbTU0U1ZWVWRrWTRRM2x2UWs1TlpVUk9SazlGVlVsRWRteGhjWGx2UWxWRFdubG9WbTFvZGpORlVUSnBkaTFaUTFCcFptZFRabWRDVWt0YVNuaGtOalZCWVhkcGJFNXpaV2xOTTFCMk0yaE1jMDV0ZGxsUlRFc3dObWxsYUcxa1lrTkhkemhuU25OaWFWZE1kVUZzZDBwWFdWbzFiRWhEZFhGTFFXWTBPVzl5VjJOUk4zaGtPRDA9IiwgInR0eXBlIjogIlQiLCAiaXNzIjogImh0dHBzOi8vMTI3LjAuMC4xOjgwMDAiLCAiaWF0IjogMTYyMTc3NzMwNSwgImV4cCI6IDE2MjE3ODA5MDV9.pVqxUNznsoZu9ND18IEMJIHDOT6_HxzoFiTLsniNdbAdXTuOoiaKeRTqtDyjT9WuUPszdHkVjt5xxeFX8gQMuA"
98 |
99 | data = {
100 | 'token': TOKEN,
101 | 'token_type_hint': 'access_token'
102 | }
103 |
104 | _basic_secret = base64.b64encode(
105 | f'{"oLyRj7sJJ3XvAYjeDCe8rQ"}:{"53fb49f2a6501ec775355c89750dc416744a3253138d5a04e409b313"}'.encode()
106 | )
107 | headers = {
108 | 'Authorization': f"Basic {_basic_secret.decode()}"
109 | }
110 |
111 | requests.post('https://127.0.0.1:8000/introspection', verify=False, data=data, headers=headers)
112 |
113 |
114 | oidc-op will return a json response like this::
115 |
116 | {
117 | "active": true,
118 | "scope": "openid profile email address phone",
119 | "client_id": "oLyRj7sJJ3XvAYjeDCe8rQ",
120 | "token_type": "access_token",
121 | "exp": 0,
122 | "iat": 1621777305,
123 | "sub": "a7b0dea2958aec275a789d7d7dc8e7d09c6316dd4fc6ae92742ed3297e14dded",
124 | "iss": "https://127.0.0.1:8000",
125 | "aud": [
126 | "oLyRj7sJJ3XvAYjeDCe8rQ"
127 | ]
128 | }
129 |
--------------------------------------------------------------------------------
/docs/source/diagrams/session_relations.mermaid:
--------------------------------------------------------------------------------
1 | erDiagram
2 | Relying-Party ||--|{ Grant : relying_party_id
3 | Relying-Party {
4 | String client_id
5 | String client_salt
6 | String registration_access_token
7 | String registration_client_uri
8 | String client_id_issued_at
9 | String client_secret
10 | datetime client_secret_expires_at
11 | String application_type
12 | List response_types
13 | List contacts
14 | String token_endpoint_auth_method
15 | List response_types
16 | List contacts
17 | String token_endpoint_auth_method
18 | List post_logout_redirect_uris
19 | String jwks_uri
20 | String frontchannel_logout_uri
21 | String frontchannel_logout_session_required
22 | String backchannel_logout_uri
23 | String grant_types
24 | List redirect_uris
25 | }
26 | User ||--o{ Grant : user_id
27 | User {
28 | string username
29 | string firstName
30 | string lastName
31 | string email
32 | }
33 | Grant {
34 | int user_id
35 | int relying_party
36 | String session_key
37 | String token_type
38 | JSon authentication_event
39 | menthod1()
40 | }
41 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | Welcome to Idpy OIDC-op Documentation
2 | ======================================
3 |
4 | .. image:: _images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png
5 | :width: 300
6 | :alt: OIDC Certified
7 |
8 | This project is a Python implementation of an **OIDC Provider** on top of `jwtconnect.io `_
9 | that shows you how to 'build' an OP using the classes and functions provided by oidc-op.
10 |
11 | If you are just going to build a standard OP you only have to write the configuration file. If you want to add or replace functionality this documentation
12 | should be able to tell you how.
13 |
14 | Idpy OIDC-op implements the following standards:
15 |
16 | * `OpenID Connect Core 1.0 incorporating errata set 1 `_
17 | * `Web Finger `_
18 | * `OpenID Connect Discovery 1.0 incorporating errata set 1 `_
19 | * `OpenID Connect Dynamic Client Registration 1.0 incorporating errata set 1 `_
20 | * `OpenID Connect Session Management 1.0 `_
21 | * `OpenID Connect Back-Channel Logout 1.0 `_
22 | * `OpenID Connect Front-Channel Logout 1.0 `_
23 | * `OAuth2 Token introspection `_
24 |
25 |
26 | It also comes with the following `add_on` modules.
27 |
28 | * Custom scopes, that extends `[OIDC standard ScopeClaims] `_
29 | * `Proof Key for Code Exchange by OAuth Public Clients (PKCE) `_
30 | * `OAuth2 PAR `_
31 | * `OAuth2 RAR `_
32 | * `OAuth2 DPoP `_
33 |
34 | The entire project code is open sourced and therefore licensed
35 | under the `Apache 2.0 `_.
36 |
37 |
38 | .. toctree::
39 | :maxdepth: 2
40 | :caption: Introduction
41 |
42 | contents/intro.rst
43 |
44 | .. toctree::
45 | :maxdepth: 2
46 | :caption: Setup
47 |
48 | contents/setup.rst
49 |
50 | .. toctree::
51 | :maxdepth: 2
52 | :caption: Configuration
53 |
54 | contents/conf.rst
55 |
56 | .. toctree::
57 | :maxdepth: 2
58 | :caption: Usage
59 |
60 | contents/usage.md
61 |
62 | .. toctree::
63 | :maxdepth: 2
64 | :caption: Session management
65 |
66 | contents/session_management.rst
67 |
68 | .. toctree::
69 | :maxdepth: 2
70 | :caption: Developer's
71 |
72 | contents/developers.md
73 |
74 | .. toctree::
75 | :maxdepth: 2
76 | :caption: Client database
77 |
78 | contents/clients.rst
79 |
80 | .. toctree::
81 | :maxdepth: 2
82 | :caption: FAQ
83 |
84 | contents/faq.md
85 |
--------------------------------------------------------------------------------
/example/django_op/README.md:
--------------------------------------------------------------------------------
1 | # django-oidc-op
2 |
3 | The Django oidc-op implementation is available here [django-oidc-op github page](https://github.com/peppelinux/django-oidc-op/tree/develop).
4 |
--------------------------------------------------------------------------------
/example/fastapi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IdentityPython/oidc-op/2f81e246ff9ef412f5dc786cd2cfa5dd5fccf9e1/example/fastapi/__init__.py
--------------------------------------------------------------------------------
/example/fastapi/main.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 |
4 | from fastapi import Depends
5 | from fastapi import FastAPI
6 | from fastapi import HTTPException
7 | from fastapi.logger import logger
8 | from fastapi.openapi.models import Response
9 | from models import AuthorizationRequest
10 | from models import WebFingerRequest
11 | from utils import verify
12 |
13 | from oidcop.exception import FailedAuthentication
14 | from oidcop.server import Server
15 |
16 | logger.setLevel(logging.DEBUG)
17 |
18 | app = FastAPI()
19 | app.server = None
20 |
21 |
22 | def get_app():
23 | return app
24 |
25 |
26 | @app.on_event("startup")
27 | def op_startup():
28 | _str = open('config.json').read()
29 | cnf = json.loads(_str)
30 | server = Server(cnf, cwd="/oidc")
31 | app.server = server
32 |
33 |
34 | @app.get("/.well-known/webfinger")
35 | async def well_known(model: WebFingerRequest = Depends()):
36 | endpoint = app.server.server_get("endpoint", "discovery")
37 | args = endpoint.process_request(model.dict())
38 | response = endpoint.do_response(**args)
39 | resp = json.loads(response["response"])
40 | return resp
41 |
42 |
43 | @app.get("/.well-known/openid-configuration")
44 | async def openid_config():
45 | endpoint = app.server.server_get("endpoint", "provider_config")
46 | args = endpoint.process_request()
47 | response = endpoint.do_response(**args)
48 | resp = json.loads(response["response"])
49 | return resp
50 |
51 |
52 | @app.post('/verify/user', status_code=200)
53 | def verify_user(kwargs: dict, response: Response):
54 | authn_method = app.server.server_get(
55 | "endpoint_context").authn_broker.get_method_by_id('user')
56 | try:
57 | return verify(app, authn_method, kwargs, response)
58 | except FailedAuthentication as exc:
59 | raise HTTPException(404, "Failed authentication")
60 |
61 |
62 | @app.get('/authorization')
63 | def authorization(model: AuthorizationRequest = Depends()):
64 | return service_endpoint(app.server.server_get("endpoint", 'authorization'))
65 |
--------------------------------------------------------------------------------
/example/fastapi/models.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from typing import Optional
3 |
4 | from pydantic import BaseModel
5 |
6 |
7 | class WebFingerRequest(BaseModel):
8 | rel: Optional[str] = 'http://openid.net/specs/connect/1.0/issuer'
9 | resource: str
10 |
11 |
12 | class AuthorizationRequest(BaseModel):
13 | acr_values: Optional[List[str]]
14 | claims: Optional[dict]
15 | claims_locales: Optional[List[str]]
16 | client_id: str
17 | display: Optional[str]
18 | id_token_hint: Optional[str]
19 | login_hint: Optional[str]
20 | max_age: Optional[int]
21 | nonce: Optional[str]
22 | prompt: Optional[List[str]]
23 | redirect_uri: str
24 | registration: Optional[dict]
25 | request: Optional[str]
26 | request_uri: Optional[str]
27 | response_mode: Optional[str]
28 | response_type: List[str]
29 | scope: List[str]
30 | state: Optional[str]
31 | ui_locales: Optional[List[str]]
32 |
--------------------------------------------------------------------------------
/example/fastapi/passwd.json:
--------------------------------------------------------------------------------
1 | {
2 | "diana": "krall",
3 | "babs": "howes",
4 | "upper": "crust"
5 | }
--------------------------------------------------------------------------------
/example/fastapi/users.json:
--------------------------------------------------------------------------------
1 | {
2 | "diana": {
3 | "sub": "dikr0001",
4 | "name": "Diana Krall",
5 | "given_name": "Diana",
6 | "family_name": "Krall",
7 | "nickname": "Dina",
8 | "email": "diana@example.org",
9 | "email_verified": false,
10 | "phone_number": "+46 90 7865000",
11 | "address": {
12 | "street_address": "Umeå Universitet",
13 | "locality": "Umeå",
14 | "postal_code": "SE-90187",
15 | "country": "Sweden"
16 | }
17 | },
18 | "babs": {
19 | "sub": "babs0001",
20 | "name": "Barbara J Jensen",
21 | "given_name": "Barbara",
22 | "family_name": "Jensen",
23 | "nickname": "babs",
24 | "email": "babs@example.com",
25 | "email_verified": true,
26 | "address": {
27 | "street_address": "100 Universal City Plaza",
28 | "locality": "Hollywood",
29 | "region": "CA",
30 | "postal_code": "91608",
31 | "country": "USA"
32 | }
33 | },
34 | "upper": {
35 | "sub": "uppe0001",
36 | "name": "Upper Crust",
37 | "given_name": "Upper",
38 | "family_name": "Crust",
39 | "email": "uc@example.com",
40 | "email_verified": true
41 | }
42 | }
--------------------------------------------------------------------------------
/example/fastapi/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from fastapi import HTTPException
4 | from fastapi import status
5 | from oic.oic import AuthorizationRequest
6 | from oidcmsg.oauth2 import ResponseMessage
7 |
8 |
9 | def do_response(endpoint, req_args, response, error='', **args):
10 | info = endpoint.do_response(request=req_args, error=error, **args)
11 |
12 | try:
13 | _response_placement = info['response_placement']
14 | except KeyError:
15 | _response_placement = endpoint.response_placement
16 |
17 | if error:
18 | if _response_placement == 'body':
19 | raise HTTPException(400, info['response'])
20 | else: # _response_placement == 'url':
21 | response.status_code = status.HTTP_307_TEMPORARY_REDIRECT
22 | resp = json.loads(info['response'])
23 | else:
24 | if _response_placement == 'body':
25 | resp = json.loads(info['response'])
26 | else: # _response_placement == 'url':
27 | response.status_code = status.HTTP_307_TEMPORARY_REDIRECT
28 | resp = json.loads(info['response'])
29 |
30 | for key, value in info['http_headers']:
31 | response.headers[key] = value
32 |
33 | if 'cookie' in info:
34 | for d in info["cookie"]:
35 | response.set_cookie(key=d["name"], value=d["value"])
36 |
37 | return resp
38 |
39 |
40 | def verify(app, authn_method, kwargs, response):
41 | """
42 | Authentication verification
43 |
44 | :param kwargs: response arguments
45 | :return: HTTP redirect
46 | """
47 |
48 | #kwargs = dict([(k, v) for k, v in request.form.items()])
49 | username = authn_method.verify(**kwargs)
50 | if not username:
51 | raise HTTPException(403, "Authentication failed")
52 |
53 | auth_args = authn_method.unpack_token(kwargs['token'])
54 | authz_request = AuthorizationRequest().from_urlencoded(auth_args['query'])
55 |
56 | endpoint = app.server.server_get("endpoint", 'authorization')
57 | _session_id = endpoint.create_session(authz_request, username, auth_args['authn_class_ref'],
58 | auth_args['iat'], authn_method)
59 |
60 | args = endpoint.authz_part2(request=authz_request, session_id=_session_id)
61 |
62 | if isinstance(args, ResponseMessage) and 'error' in args:
63 | raise HTTPException(400, args.to_json())
64 |
65 | return do_response(endpoint, kwargs, response, **args)
66 |
67 |
68 | IGNORE = ["cookie", "user-agent"]
69 |
70 |
71 | def service_endpoint(app, endpoint):
72 | _log = app.srv_config.logger
73 | _log.info('At the "{}" endpoint'.format(endpoint.name))
74 |
75 | http_info = {
76 | "headers": {k: v for k, v in request.headers.items(lower=True) if k not in IGNORE},
77 | "method": request.method,
78 | "url": request.url,
79 | # name is not unique
80 | "cookie": [{"name": k, "value": v} for k, v in request.cookies.items()]
81 | }
82 |
83 | if request.method == 'GET':
84 | try:
85 | req_args = endpoint.parse_request(request.args.to_dict(), http_info=http_info)
86 | except (InvalidClient, UnknownClient) as err:
87 | _log.error(err)
88 | return make_response(json.dumps({
89 | 'error': 'unauthorized_client',
90 | 'error_description': str(err)
91 | }), 400)
92 | except Exception as err:
93 | _log.error(err)
94 | return make_response(json.dumps({
95 | 'error': 'invalid_request',
96 | 'error_description': str(err)
97 | }), 400)
98 | else:
99 | if request.data:
100 | if isinstance(request.data, str):
101 | req_args = request.data
102 | else:
103 | req_args = request.data.decode()
104 | else:
105 | req_args = dict([(k, v) for k, v in request.form.items()])
106 | try:
107 | req_args = endpoint.parse_request(req_args, http_info=http_info)
108 | except Exception as err:
109 | _log.error(err)
110 | err_msg = ResponseMessage(error='invalid_request', error_description=str(err))
111 | return make_response(err_msg.to_json(), 400)
112 |
113 | _log.info('request: {}'.format(req_args))
114 | if isinstance(req_args, ResponseMessage) and 'error' in req_args:
115 | return make_response(req_args.to_json(), 400)
116 |
117 | try:
118 | if isinstance(endpoint, Token):
119 | args = endpoint.process_request(AccessTokenRequest(**req_args), http_info=http_info)
120 | else:
121 | args = endpoint.process_request(req_args, http_info=http_info)
122 | except Exception as err:
123 | message = traceback.format_exception(*sys.exc_info())
124 | _log.error(message)
125 | err_msg = ResponseMessage(error='invalid_request', error_description=str(err))
126 | return make_response(err_msg.to_json(), 400)
127 |
128 | _log.info('Response args: {}'.format(args))
129 |
130 | if 'redirect_location' in args:
131 | return redirect(args['redirect_location'])
132 | if 'http_response' in args:
133 | return make_response(args['http_response'], 200)
134 |
135 | response = do_response(endpoint, req_args, **args)
136 | return response
137 |
--------------------------------------------------------------------------------
/example/flask_op/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:stable-slim
2 |
3 | MAINTAINER Roland Hedberg "roland@catalogix.se"
4 |
5 | COPY . /app
6 | ENV SRCDIR /app/src
7 |
8 | RUN apt-get update && apt-get install -y --no-install-recommends \
9 | git \
10 | curl \
11 | libssl-dev \
12 | libffi-dev \
13 | python3-pip \
14 | python3-setuptools && apt-get clean && rm -rf /var/lib/apt/lists/*
15 |
16 | RUN git clone --depth=1 https://github.com/rohe/oidc-op.git ${SRCDIR}/oidc-op
17 | WORKDIR ${SRCDIR}/oidc-op
18 | RUN python3 setup.py install
19 |
20 | RUN pip3 install ndg-httpsclient
21 |
22 | WORKDIR /app
23 | RUN pip3 install -r requirements.txt
24 | EXPOSE 5000
25 | CMD python3 ./server.py config.yaml
26 |
--------------------------------------------------------------------------------
/example/flask_op/README.md:
--------------------------------------------------------------------------------
1 | # OIDC-op example application
2 | To run the Flask based example application, execute the following commands:
3 |
4 | ```bash
5 | cd flask_op/
6 | pip install -r requirements.txt # install the dependencies
7 | ./server.py config.yaml
8 | ```
--------------------------------------------------------------------------------
/example/flask_op/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IdentityPython/oidc-op/2f81e246ff9ef412f5dc786cd2cfa5dd5fccf9e1/example/flask_op/__init__.py
--------------------------------------------------------------------------------
/example/flask_op/application.py:
--------------------------------------------------------------------------------
1 | import os
2 | from urllib.parse import urlparse
3 |
4 | from flask.app import Flask
5 |
6 | from oidcop.server import Server
7 |
8 | folder = os.path.dirname(os.path.realpath(__file__))
9 |
10 |
11 | def init_oidc_op(app):
12 | _op_config = app.srv_config
13 |
14 | server = Server(_op_config, cwd=folder)
15 |
16 | for endp in server.endpoint.values():
17 | p = urlparse(endp.endpoint_path)
18 | _vpath = p.path.split('/')
19 | if _vpath[0] == '':
20 | endp.vpath = _vpath[1:]
21 | else:
22 | endp.vpath = _vpath
23 |
24 | return server
25 |
26 |
27 | def oidc_provider_init_app(op_config, name=None, **kwargs):
28 | name = name or __name__
29 | app = Flask(name, static_url_path='', **kwargs)
30 | app.srv_config = op_config
31 |
32 | try:
33 | from .views import oidc_op_views
34 | except ImportError:
35 | from views import oidc_op_views
36 |
37 | app.register_blueprint(oidc_op_views)
38 |
39 | # Initialize the oidc_provider after views to be able to set correct urls
40 | app.server = init_oidc_op(app)
41 |
42 | return app
43 |
--------------------------------------------------------------------------------
/example/flask_op/certs/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIFUDCCAzigAwIBAgIJAJWgBcizyJrFMA0GCSqGSIb3DQEBCwUAMD0xCzAJBgNV
3 | BAYTAlNFMQ0wCwYDVQQKDARPSURGMR8wHQYDVQQDDBZGZWQgYXdhcmUgUlAgdGVz
4 | dCB0b29sMB4XDTE3MDIxMzE5MDg1MloXDTE4MDIxMzE5MDg1MlowPTELMAkGA1UE
5 | BhMCU0UxDTALBgNVBAoMBE9JREYxHzAdBgNVBAMMFkZlZCBhd2FyZSBSUCB0ZXN0
6 | IHRvb2wwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC3NrEL+VKs00NT
7 | R+ZpGRxvDoeLhD7EM+uf7IqHl6IN3H6pflAOE8YqnTepdglhGH4a7nyftINTZjDU
8 | 86anR+OKPoY2Padf4E+YceJOcaT6lB5XOWxBu4j3wDRHb6jMUwMDUXHsmh389Bvx
9 | X44KSYe/mhjkrIV8bolhT9NpNjPVUdUvpwpSxDOhSjq7BCmfdvXJrNNYElEQaDSc
10 | yJ4h6BAOp/FfdnWKAeiVDpIF5QqZgr0gzKiV5LEvwsNfHynsLgrlgK2+Fd8qIqbC
11 | /fHtB1BEL3h01dlBR1Y4ocMM5we23Phe4lwQs8QojPTnnr14fWynrjNi0Km0TcMT
12 | TDHVnw5qO5dSr4LpBcfIo82YWpj6lTEKQwKin+SPz0k0kD4E83rtsGp8n3FWHVAo
13 | BsIJ4O58REi3YTh1NCe/bjsQWiFOPW0N9GOl0UTOUj90cGVbO9i91aDFHHQWOIiA
14 | VsmZ35yOjQ031It9Kzv4YcmWXQcdKYnzUQ5eSXZPmJFoebKgQF6neFlg1hp6uDKi
15 | NRxkaPWGVCVZXPmgRwVcFdbxI8OpNqPEFQGskUPGJS5CF1o8o6wuVwPSSwxDVoYM
16 | 12TTdATH1he4cK69ej/1F2oHCVQ0KE46fNABaxNKxGls0bPPPJBPrQBjoAR2qxgg
17 | iFz2DjumVC3EySwXLsH4tXTjyuVbSQIDAQABo1MwUTAdBgNVHQ4EFgQUiD0bTabj
18 | Q0Pf0vVJneGr5TQRO+4wHwYDVR0jBBgwFoAUiD0bTabjQ0Pf0vVJneGr5TQRO+4w
19 | DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAnYCE5MdqVXHBxMGZ
20 | 1bIZxwLg9pe5poaX7l7XGdXxnBKWxfqwCx2UHQZIBdV3eIt8lgtuOL1en9ZCHAIY
21 | X0OZCafQ1Jzx3nXV4qOoolfmri0DQs60LPozoXKW61mah8fFhf/XdjuZxYH+XVV6
22 | 39E08MY4ZWDzzNoDe5zhGWw+IOfowx5wNTtZ8CipWUv4FiO9cUZJ/1hnJgE0CQNH
23 | v4v0g0lIuWs7eArbzvxTu3jHWx/+eYvl2TSYxEHpVulbesnI27M34nS0OePqbywO
24 | eGBtM65UuCCBh27FO+O7qJWA3sRPuw/cll0vi69WVYHO5rk7yji1hiTT2MKTEizP
25 | GmdT/FXG4nEsM6WaEe4FMJN6cZf49BUzRcEdW6k8i2YIysHf8fi3Xv1JF74OB5bF
26 | TogV/Fu/LzXsfA/XTj9ki0hUNmueyNT/xBD5tOH4FqHQvMWpjpzfwI90ENVeY+Ad
27 | BCU2Ck1HBEuUhUNaC1d6QkU6pn3voPvaWK49+T9NyrFVMNHVWHeLUHJ/i9kgWXLl
28 | TgAbTCmnJOHTxxCVCf40EjOpPR3hlCadYr8vOGyuHPk1M2Lppgh2kQtFX5ubhhfW
29 | IKP5TPKuZlu3z9RjfUvIxqWC6cbwjlOGIx2K0uCnIbpTzTuaLHJSWWRUpDzNL6lg
30 | V620B7/n1jo2JDudjhjD2uLekJg=
31 | -----END CERTIFICATE-----
32 |
--------------------------------------------------------------------------------
/example/flask_op/certs/client.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEvjCCAqagAwIBAgIJANUReZOMl8O2MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
3 | BAMMCTEyNy4wLjAuMTAeFw0yMDAyMDUxOTUyNDZaFw0zMDAyMDIxOTUyNDZaMBQx
4 | EjAQBgNVBAMMCTEyNy4wLjAuMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
5 | ggIBAJxWxxEGC3dDWTgVNkKf0XvgKpWo8tIQHj78W/qAogki+dPjfmvQWTcv0AEu
6 | P+o9nxUTDb5oWe24/hcqPeCuC6k33bMbq+boEXAeCUSxXCOzefTd6QLw3S4G8coQ
7 | qsH80dGsis9Hr29ubUq1GWWOR1CMMs8gbeIRto5R7FnbMim2Qbnm+KYpOmoo9L8w
8 | EmqFDARowB+OYzu3LDVVaREGs/lURy9YkFbahrrp1KIxYeqp+KMhLiHE4JYBe5p8
9 | hQDnoJNsYZNe0zNWdiB3TRIR0A1mSq4SFnWQqMVPtcfZ6z/FUeGnOohIEeXdf/Nu
10 | RpvqsQy4/WzaXBXxwE910gaH/frEdq3+JBY8mtvNUlaaIfIF1G7V3nJS9GuOSIKR
11 | Rz5sWUgNumexDjEGLMeh1GSYCMtQU3IGLOBj+ZjHjLlNO4AqzNGmyr8Xe9NtA+se
12 | zazOYMyFjRWfA/o80IqIorfc0ifZvD6Y3wmsFi8UPsMVH15QHAiaiiZHMptKOXpr
13 | ChPDiyIIPIcO0AfBkxPPrY27/jtq9Su1JlcXUV7Hq0yWqJC1RhNE34+kKQEt32NQ
14 | T5NhjVF02s8o3s3SH1h9DzJ9wu/3bzjSwYTvX4l7F87RNvd1eH58Fb3PqkTaUxpK
15 | wU8Mbp5WDwJt24YQ31Fwr/dpsiArGjiymjU6Cp+FG5P9FECpAgMBAAGjEzARMA8G
16 | A1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBACR4AW0312op+5CZa7ow
17 | fBQ4m+dnsHGmMQFCzFH7EaBej/mTcZ9RV+meNPZ5yvazK6Pr0Daagp1dMsn+YfGu
18 | 8YttyzYdbmNzKq+6OJemaAcn/0MfzQjeP+cjbfW+yTL1Xf+Qfw5MSIrep9izgcEt
19 | 7kYCzDHCxfiLudiirIVxxWPeelN+43SiFnzGz3iISmGHx/UOP+nYfyfKiqzWy+CI
20 | TnQmu+MQMfsAzCO88ZTko61vpXab2nsbX1v1Br8ZKGkDFZ1NoV4EmAV5TyCeHwA5
21 | SuFhWrWID3SvZQHW7W6rlt8hk8kJG2523zjW38A3tdK4M3mxT8F02ru934JDr+DQ
22 | CX9OslO8P1wk4WpbpezAO8pLrtJWDxMTRccIRft9Nqw03QzJ0sS7NPw7IehBHf/z
23 | C/TLre1Mt4OSbowVkNDZztxensIvUHi9lcf8a3GM4hMlkQdk0DFLwVJ3/XSYHnC9
24 | ZDkPiHJyYdR+DUVBlc5mgxUyOWto4pZ9UmVpXtBbDP19jO5X4QKRs9BgiZl2pz0d
25 | mW1LZxkbBJneHlm3/2xZpK2K8jdLjvb3LC0VtGp1aeKAlVjL8yHOaFZeE8lLbx00
26 | L3XueHme3PzU69Leq13XGL1WiCFYGScIP6zBB0JFlkrLQnviXXyGWUAIjVoNRUYh
27 | HuNhZgj3qbjPBXTC3Dkno1d3
28 | -----END CERTIFICATE-----
29 |
--------------------------------------------------------------------------------
/example/flask_op/certs/client.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | 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
51 | ZCZCLsaKMvisUtAXXmxvw8BM2txzoQ==
52 | -----END PRIVATE KEY-----
53 |
--------------------------------------------------------------------------------
/example/flask_op/certs/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | 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
51 | Svp9rK9sO9LqfKBeV9NKW9/wb/X6lU4=
52 | -----END PRIVATE KEY-----
53 |
--------------------------------------------------------------------------------
/example/flask_op/passwd.json:
--------------------------------------------------------------------------------
1 | {
2 | "diana": "krall",
3 | "babs": "howes",
4 | "upper": "crust"
5 | }
--------------------------------------------------------------------------------
/example/flask_op/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask
--------------------------------------------------------------------------------
/example/flask_op/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | python3 server.py config.json
4 |
--------------------------------------------------------------------------------
/example/flask_op/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import argparse
3 | import json
4 | import logging
5 | import os
6 |
7 | from oidcmsg.configure import Configuration
8 | from oidcmsg.configure import create_from_config_file
9 |
10 | from oidcop.configure import OPConfiguration
11 | from oidcop.utils import create_context
12 |
13 | try:
14 | from .application import oidc_provider_init_app
15 | except (ModuleNotFoundError, ImportError):
16 | from application import oidc_provider_init_app
17 |
18 | dir_path = os.path.dirname(os.path.realpath(__file__))
19 |
20 | logger = logging.getLogger(__name__)
21 |
22 |
23 | # class PeerCertWSGIRequestHandler(werkzeug.serving.WSGIRequestHandler):
24 | # """
25 | # We subclass this class so that we can gain access to the connection
26 | # property. self.connection is the underlying client socket. When a TLS
27 | # connection is established, the underlying socket is an instance of
28 | # SSLSocket, which in turn exposes the getpeercert() method.
29 | #
30 | # The output from that method is what we want to make available elsewhere
31 | # in the application.
32 | # """
33 | #
34 | # def make_environ(self):
35 | # """
36 | # The superclass method develops the environ hash that eventually
37 | # forms part of the Flask request object.
38 | #
39 | # We allow the superclass method to run first, then we insert the
40 | # peer certificate into the hash. That exposes it to us later in
41 | # the request variable that Flask provides
42 | # """
43 | # environ = super(PeerCertWSGIRequestHandler, self).make_environ()
44 | # x509_binary = self.connection.getpeercert(True)
45 | # if x509_binary:
46 | # x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509_binary)
47 | # environ['peercert'] = x509
48 | # else:
49 | # logger.warning('No peer certificate')
50 | # environ['peercert'] = ''
51 | # return environ
52 |
53 |
54 | def main(config_file, args):
55 | logging.basicConfig(level=logging.DEBUG)
56 | config = create_from_config_file(Configuration,
57 | entity_conf=[{
58 | "class": OPConfiguration, "attr": "op",
59 | "path": ["op", "server_info"]
60 | }],
61 | filename=config_file,
62 | base_path=dir_path)
63 | app = oidc_provider_init_app(config.op, 'oidc_op')
64 | app.logger = config.logger
65 |
66 | web_conf = config.web_conf
67 |
68 | context = create_context(dir_path, web_conf)
69 |
70 | if args.display:
71 | print(json.dumps(app.endpoint_context.provider_info, indent=4, sort_keys=True))
72 | exit(0)
73 |
74 | kwargs = {}
75 | if context:
76 | kwargs["ssl_context"] = context
77 | # kwargs["request_handler"] = PeerCertWSGIRequestHandler
78 |
79 | app.run(host=web_conf['domain'], port=web_conf['port'], debug=web_conf['debug'], **kwargs)
80 |
81 |
82 | if __name__ == '__main__':
83 | parser = argparse.ArgumentParser()
84 | parser.add_argument('-d', dest='display', action='store_true')
85 | parser.add_argument('-t', dest='tls', action='store_true')
86 | parser.add_argument('-k', dest='insecure', action='store_true')
87 | parser.add_argument(dest="config")
88 | args = parser.parse_args()
89 | main(args.config, args)
90 |
--------------------------------------------------------------------------------
/example/flask_op/templates/check_session_iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Session Management - OP iframe
6 |
9 |
10 |
11 |
12 |
13 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/example/flask_op/templates/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Error: {{ title }}
5 |
6 | {% if redirect_url is defined %}
7 | Continue
8 | {% else %}
9 | {% endif %}
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/flask_op/templates/frontchannel_logout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Logout
5 |
6 |
7 |
10 |
11 |
12 |
29 | {{ frames|safe }}
30 |
31 |