├── .gitignore
├── LICENSE
├── Makefile
├── README.ja.md
├── README.md
├── api
├── __init__.py
├── admin.py
├── apps.py
├── authorization_decision_endpoint.py
├── authorization_endpoint.py
├── authorization_page_model.py
├── base_endpoint.py
├── introspection_endpoint.py
├── migrations
│ └── __init__.py
├── models.py
├── spi
│ ├── __init__.py
│ ├── authorization_request_decision_handler_spi_impl.py
│ ├── authorization_request_handler_spi_impl.py
│ ├── no_interaction_handler_spi_impl.py
│ └── token_request_handler_spi_impl.py
├── static
│ └── api
│ │ └── css
│ │ └── authorization.css
├── templates
│ └── api
│ │ └── authorization.html
├── tests.py
├── urls.py
└── views.py
├── authlete.ini
├── backends
├── __init__.py
└── cognito_backend.py
├── django_oauth_server
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
└── manage.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *.py[cdo]
3 | *$py.class
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | #==================================================
19 | # VARIABLES
20 | #==================================================
21 | PYTHON = python3
22 |
23 |
24 | #==================================================
25 | # TARGETS
26 | #==================================================
27 | .PHONY: _default clean clean-python help run shell
28 |
29 |
30 | _default: help
31 |
32 |
33 | clean: clean-python
34 |
35 |
36 | clean-python:
37 | @find . -name "__pycache__" -prune -exec rm -rf '{}' \;
38 | @find . -name "*.py[cdo]" -exec rm -rf '{}' \;
39 |
40 |
41 | help:
42 | @printf '%s\n' \
43 | "clean - removes generated files." \
44 | "clean-python - removes files generated by python." \
45 | "help - shows this help text." \
46 | "run - starts the web servier for development." \
47 | "shell - starts a Python shell."
48 |
49 |
50 | run:
51 | $(PYTHON) manage.py runserver
52 |
53 |
54 | shell:
55 | $(PYTHON) manage.py shell
56 |
--------------------------------------------------------------------------------
/README.ja.md:
--------------------------------------------------------------------------------
1 | 認可サーバー実装 (Python)
2 | =========================
3 |
4 | 概要
5 | ----
6 |
7 | [OAuth 2.0][RFC6749] と [OpenID Connect][OIDC] をサポートする認可サーバーの Python
8 | による実装です。
9 |
10 | この実装は Django API と authlete-python-django ライブラリを用いて書かれています。
11 | [Django][Django] は Python で書かれた Web フレームワークの一つです。
12 | [authlete-python-django][AuthletePythonDjango]
13 | は、認可サーバーとリソースサーバーを実装するためのユーティリティークラス群を提供するオープンソースライブラリです。
14 | authlete-python-django は [authlete-python][AuthletePython] ライブラリを使用しており、こちらは
15 | [Authlete Web API][AuthleteAPI] とやりとりするためのオープンソースライブラリです。
16 |
17 | この認可サーバーにより発行されたアクセストークンは、Authlete
18 | をバックエンドサービスとして利用しているリソースサーバーに対して使うことができます。
19 | [django-resource-server][DjangoResourceServer] はそのようなリソースサーバーの実装です。
20 | [OpenID Connect Core 1.0][OIDCCore]
21 | で定義されている[ユーザー情報エンドポイント][UserInfoEndpoint]をサポートし、
22 | 保護リソースエンドポイントの実装例も含んでいます。
23 |
24 | ライセンス
25 | ----------
26 |
27 | Apache License, Version 2.0
28 |
29 | ソースコード
30 | ------------
31 |
32 | https://github.com/authlete/django-oauth-server
33 |
34 | Authlete について
35 | -----------------
36 |
37 | [Authlete][Authlete] (オースリート) は、OAuth 2.0 & OpenID Connect
38 | の実装をクラウドで提供するサービスです ([概説][AuthleteOverview])。
39 | Authlete が提供するデフォルト実装を使うことにより、もしくはこの実装
40 | (django-oauth-server) でおこなっているように [Authlete Web API][AuthleteAPI]
41 | を用いて認可サーバーを自分で実装することにより、OAuth 2.0 と OpenID Connect
42 | の機能を簡単に実現できます。
43 |
44 | この認可サーバーの実装を使うには、Authlete から API
45 | クレデンシャルズを取得し、`authlete.ini` に設定する必要があります。
46 | API クレデンシャルズを取得する手順はとても簡単です。
47 | 単にアカウントを登録するだけで済みます ([サインアップ][AuthleteSignUp])。
48 | 詳細は[クイックガイド][AuthleteGettingStarted]を参照してください。
49 |
50 |
51 | 実行方法
52 | --------
53 |
54 | 1. authlete-python ライブラリと authlete-python-django ライブラリをインストールします。
55 |
56 | $ pip install authlete
57 | $ pip install authlete-django
58 |
59 | 2. この認可サーバーの実装をダウンロードします。
60 |
61 | $ git clone https://github.com/authlete/django-oauth-server.git
62 | $ cd django-oauth-server
63 |
64 | 3. 設定ファイルを編集して API クレデンシャルズをセットします。
65 |
66 | $ vi authlete.ini
67 |
68 | 4. テスト用のユーザーアカウントを作成します。
69 |
70 | $ python manage.py migrate
71 | $ python manage.py shell
72 | >>> from django.contrib.auth.models import User
73 | >>> user = User()
74 | >>> user.username = 'john'
75 | >>> user.first_name = 'John'
76 | >>> user.last_name = 'Smith'
77 | >>> user.email = 'john@example.com'
78 | >>> user.set_password('john')
79 | >>> user.is_active = True
80 | >>> user.save()
81 | >>> quit()
82 |
83 | 5. `http://localhost:8000` で認可サーバーを起動します。
84 |
85 | $ python manage.py runserver
86 |
87 | エンドポイント
88 | --------------
89 |
90 | この実装は、下表に示すエンドポイントを公開します。
91 |
92 | | エンドポイント | パス |
93 | |:-----------------------------------|:------------------------------------|
94 | | 認可エンドポイント | `/api/authorization` |
95 | | トークンエンドポイント | `/api/token` |
96 | | JWK Set エンドポイント | `/api/jwks` |
97 | | 設定エンドポイント | `/.well-known/openid-configuration` |
98 | | 取り消しエンドポイント | `/api/revocation` |
99 | | イントロスペクションエンドポイント | `/api/introspection` |
100 |
101 | 認可エンドポイントとトークンエンドポイントは、[RFC 6749][RFC6749]、[OpenID Connect Core 1.0][OIDCCore]、
102 | [OAuth 2.0 Multiple Response Type Encoding Practices][MultiResponseType]、[RFC 7636][RFC7636]
103 | ([PKCE][PKCE])、その他の仕様で説明されているパラメーター群を受け付けます。
104 |
105 | JWK Set エンドポイントは、クライアントアプリケーションが (1) この OpenID
106 | プロバイダーによる署名を検証できるようにするため、また (2) この OpenID
107 | へのリクエストを暗号化できるようにするため、JSON Web Key Set ドキュメント
108 | (JWK Set) を公開します。
109 |
110 | 設定エンドポイントは、この OpenID プロバイダーの設定情報を
111 | [OpenID Connect Discovery 1.0][OIDCDiscovery] で定義されている JSON フォーマットで公開します。
112 |
113 | 取り消しエンドポイントはアクセストークンやリフレッシュトークンを取り消すための
114 | Web API です。 その動作は [RFC 7009][RFC7009] で定義されています。
115 |
116 | イントロスペクションエンドポイントはアクセストークンやリフレッシュトークンの情報を取得するための
117 | Web API です。 その動作は [RFC 7662][RFC7662] で定義されています。
118 |
119 | 認可リクエストの例
120 | ------------------
121 |
122 | 次の例は [Implicit フロー][ImplicitFlow]を用いて認可エンドポイントからアクセストークンを取得する例です。
123 | `{クライアントID}` となっているところは、あなたのクライアントアプリケーションの実際のクライアント
124 | ID で置き換えてください。 クライアントアプリケーションについては、[クイックガイド][AuthleteGettingStarted]
125 | および[開発者コンソール][DeveloperConsole]のドキュメントを参照してください。
126 |
127 | http://localhost:8000/api/authorization?client_id={クライアントID}&response_type=token
128 |
129 | 上記のリクエストにより、認可ページが表示されます。
130 | 認可ページでは、ログイン情報の入力と、"Authorize" ボタン (認可ボタン) もしくは "Deny" ボタン
131 | (拒否ボタン) の押下が求められます。 「実行方法」で示した通りにユーザーアカウントを作成済みであれば、
132 | ログイン ID とパスワードはどちらも `john` です。
133 |
134 | 一度ログインが成功すると、認可ページはログインフォームを表示しないかもしれません。
135 | ログインフォームを強制的に表示させるには、認可リクエストの末尾に `&prompt=login` を追加してください。
136 |
137 | Amazon Cognito
138 | --------------
139 |
140 | この実装は、[Amazon Cognito][Cognito] をユーザーデータベースとして使うサンプルコードを含んでいます。
141 | サンプルコードを有効にするには、次の手順を踏んでください。
142 |
143 | 1. AWS SDK for Python ([Boto3][Boto3]) をインストールします。
144 |
145 | $ pip install boto3
146 |
147 | 2. `django_oauth_server/settings.py` を開き、 `AUTHENTICATION_BACKENDS` に `backends.CognitoBackend` を追加します。
148 |
149 | AUTHENTICATION_BACKENDS = ('backends.CognitoBackend',)
150 |
151 | 3. 同ファイル内の `COGNITO_USER_POOL_ID` と `COGNITO_CLIENT_ID` を適切に設定します。
152 |
153 | COGNITO_USER_POOL_ID = 'YOUR_COGNITO_USER_POOL_ID'
154 | COGNITO_CLIENT_ID = 'YOUR_COGNITO_CLIENT_ID'
155 |
156 | Cognito ユーザープールに紐付く Cognito クライアントが `ALLOW_ADMIN_USER_PASSWORD_AUTH`
157 | をサポートしなければならないこと、及び、AWS アカウントが Cognito の
158 | [AdminInitiateAuth API][AdminInitiateAuth] と [AdminGetUser API][AdminGetUser]
159 | を呼ぶのに必要な権限を持っている必要があることに注意してください。
160 |
161 | 詳細は [Amazon Cognito と最新の OAuth/OIDC 仕様][CognitoTutorial] を参照してください。
162 |
163 | その他の情報
164 | ------------
165 |
166 | - [Authlete][Authlete] - Authlete ホームページ
167 | - [authlete-python][AuthletePython] - Python 用 Authlete ライブラリ
168 | - [authlete-python-django][AuthletePythonDjango] - Django (Python) 用 Authlete ライブラリ
169 | - [django-resource-server][DjangoResourceServer] - リソースサーバーの実装
170 |
171 | コンタクト
172 | ----------
173 |
174 | コンタクトフォーム : https://www.authlete.com/ja/contact/
175 |
176 | | 目的 | メールアドレス |
177 | |:-----|:---------------------|
178 | | 一般 | info@authlete.com |
179 | | 営業 | sales@authlete.com |
180 | | 広報 | pr@authlete.com |
181 | | 技術 | support@authlete.com |
182 |
183 | [AdminGetUser]: https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminGetUser.html
184 | [AdminInitiateAuth]: https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminInitiateAuth.html
185 | [Authlete]: https://www.authlete.com/ja/
186 | [AuthleteAPI]: https://docs.authlete.com/
187 | [AuthleteGettingStarted]: https://www.authlete.com/ja/developers/getting_started/
188 | [AuthleteOverview]: https://www.authlete.com/ja/developers/overview/
189 | [AuthletePython]: https://github.com/authlete/authlete-python/
190 | [AuthletePythonDjango]: https://github.com/authlete/authlete-python-django/
191 | [AuthleteSignUp]: https://so.authlete.com/accounts/signup
192 | [Boto3]: https://boto3.amazonaws.com/v1/documentation/api/latest/index.html
193 | [Cognito]: https://aws.amazon.com/cognito/
194 | [CognitoTutorial]: https://www.authlete.com/ja/developers/tutorial/cognito/
195 | [DeveloperConsole]: https://www.authlete.com/ja/developers/cd_console/
196 | [Django]: https://www.djangoproject.com/
197 | [DjangoOAuthServer]: https://github.com/authlete/django-oauth-server/
198 | [DjangoResourceServer]: https://github.com/authlete/django-resource-server/
199 | [ImplicitFlow]: https://tools.ietf.org/html/rfc6749#section-4.2
200 | [MultiResponseType]: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
201 | [OIDC]: https://openid.net/connect/
202 | [OIDCCore]: https://openid.net/specs/openid-connect-core-1_0.html
203 | [OIDCDiscovery]: https://openid.net/specs/openid-connect-discovery-1_0.html
204 | [PKCE]: https://www.authlete.com/ja/developers/pkce/
205 | [RFC6749]: https://tools.ietf.org/html/rfc6749
206 | [RFC7009]: https://tools.ietf.org/html/rfc7009
207 | [RFC7636]: https://tools.ietf.org/html/rfc7636
208 | [RFC7662]: https://tools.ietf.org/html/rfc7662
209 | [UserInfoEndpoint]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
210 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Authorization Server Implementation in Python
2 | =============================================
3 |
4 | Overview
5 | --------
6 |
7 | This is an authorization server implementation in Python which supports
8 | [OAuth 2.0][RFC6749] and [OpenID Connect][OIDC].
9 |
10 | This implementation is written using Django API and authlete-python-django
11 | library. [Django][Django] is a web framework written in Python. On the other
12 | hand, [authlete-python-django][AuthletePythonDjango] is an Authlete's open
13 | source library which provides utility classes for developers to implement
14 | an authorization server and a resource server. authlete-python-django in
15 | turn uses [authlete-python][AuthletePython] library which is another open
16 | source library to communicate with [Authlete Web APIs][AuthleteAPI].
17 |
18 | Access tokens issued by this authorization server can be used at a resource
19 | server which uses Authlete as a backend service.
20 | [django-resource-server][DjangoResourceServer] is such a resource server
21 | implementation. It supports a [userinfo endpoint][UserInfoEndpoint] defined
22 | in [OpenID Connect Core 1.0][OIDCCore] and includes an example implementation
23 | of a protected resource endpoint, too.
24 |
25 | License
26 | -------
27 |
28 | Apache License, Version 2.0
29 |
30 | Source Code
31 | -----------
32 |
33 | https://github.com/authlete/django-oauth-server
34 |
35 | About Authlete
36 | --------------
37 |
38 | [Authlete][Authlete] is a cloud service that provides an implementation of
39 | OAuth 2.0 & OpenID Connect ([overview][AuthleteOverview]). You can easily get
40 | the functionalities of OAuth 2.0 and OpenID Connect either by using the default
41 | implementation provided by Authlete or by implementing your own authorization
42 | server using [Authlete Web APIs][AuthleteAPI] as this implementation
43 | (django-oauth-server) does.
44 |
45 | To use this authorization server implementation, you need to get API credentials
46 | from Authlete and set them in `authlete.ini`. The steps to get API credentials
47 | are very easy. All you have to do is just to register your account
48 | ([sign up][AuthleteSignUp]). See [Getting Started][AuthleteGettingStarted] for
49 | details.
50 |
51 | How To Run
52 | ----------
53 |
54 | 1. Install authlete-python and authlete-python-django libraries.
55 |
56 | $ pip install authlete
57 | $ pip install authlete-django
58 |
59 | 2. Download the source code of this authorization server implementation.
60 |
61 | $ git clone https://github.com/authlete/django-oauth-server.git
62 | $ cd django-oauth-server
63 |
64 | 3. Edit the configuration file to set the API credentials of yours.
65 |
66 | $ vi authlete.ini
67 |
68 | 4. Create a user account for testing.
69 |
70 | $ python manage.py migrate
71 | $ python manage.py shell
72 | >>> from django.contrib.auth.models import User
73 | >>> user = User()
74 | >>> user.username = 'john'
75 | >>> user.first_name = 'John'
76 | >>> user.last_name = 'Smith'
77 | >>> user.email = 'john@example.com'
78 | >>> user.set_password('john')
79 | >>> user.is_active = True
80 | >>> user.save()
81 | >>> quit()
82 |
83 | 5. Start the authorization server on `http://localhost:8000`.
84 |
85 | $ python manage.py runserver
86 |
87 | Endpoints
88 | ---------
89 |
90 | This implementation exposes endpoints as listed in the table below.
91 |
92 | | Endpoint | Path |
93 | |:-------------------------------------|:------------------------------------|
94 | | Authorization Endpoint | `/api/authorization` |
95 | | Token Endpoint | `/api/token` |
96 | | JWK Set Endpoint | `/api/jwks` |
97 | | Configuration Endpoint | `/.well-known/openid-configuration` |
98 | | Revocation Endpoint | `/api/revocation` |
99 | | Introspection Endpoint | `/api/introspection` |
100 |
101 | The authorization endpoint and the token endpoint accept parameters described
102 | in [RFC 6749][RFC6749], [OpenID Connect Core 1.0][OIDCCore],
103 | [OAuth 2.0 Multiple Response Type Encoding Practices][MultiResponseType],
104 | [RFC 7636][RFC7636] ([PKCE][PKCE]) and other specifications.
105 |
106 | The JWK Set endpoint exposes a JSON Web Key Set document (JWK Set) so that
107 | client applications can (1) verify signatures signed by this OpenID Provider
108 | and (2) encrypt their requests to this OpenID Provider.
109 |
110 | The configuration endpoint exposes the configuration information of this OpenID
111 | Provider in the JSON format defined in [OpenID Connect Discovery 1.0][OIDCDiscovery].
112 |
113 | The revocation endpoint is a Web API to revoke access tokens and refresh
114 | tokens. Its behavior is defined in [RFC 7009][RFC7009].
115 |
116 | The introspection endpoint is a Web API to get information about access
117 | tokens and refresh tokens. Its behavior is defined in [RFC 7662][RFC7662].
118 |
119 | Authorization Request Example
120 | -----------------------------
121 |
122 | The following is an example to get an access token from the authorization
123 | endpoint using [Implicit Flow][ImplicitFlow]. Don't forget to replace
124 | `{client-id}` in the URL with the real client ID of one of your client
125 | applications. As for client applications, see
126 | [Getting Started][AuthleteGettingStarted] and the document of
127 | [Developer Console][DeveloperConsole].
128 |
129 | http://localhost:8000/api/authorization?client_id={client-id}&response_type=token
130 |
131 | The request above will show you an authorization page. The page asks you to
132 | input login credentials and click "Authorize" button or "Deny" button. If you
133 | have created a user account as shown in _How To Run_, both the login ID and
134 | the password are `john`.
135 |
136 | Once login succeeds, the authorization page may not show the login form.
137 | To force the login form to appear, append `&prompt=login` at the end of the
138 | authorization request.
139 |
140 | Amazon Cognito
141 | --------------
142 |
143 | This implementation contains a sample code that uses [Amazon Cognito][Cognito]
144 | as a user database. To enable the sample code, follow the steps below.
145 |
146 | 1. Install AWS SDK for Python ([Boto3][Boto3]).
147 |
148 | $ pip install boto3
149 |
150 | 2. Open `django_oauth_server/settings.py` and add `backends.CognitoBackend` to `AUTHENTICATION_BACKENDS`.
151 |
152 | AUTHENTICATION_BACKENDS = ('backends.CognitoBackend',)
153 |
154 | 3. Set `COGNITO_USER_POOL_ID` and `COGNITO_CLIENT_ID` in the same file properly.
155 |
156 | COGNITO_USER_POOL_ID = 'YOUR_COGNITO_USER_POOL_ID'
157 | COGNITO_CLIENT_ID = 'YOUR_COGNITO_CLIENT_ID'
158 |
159 | Note that the Cognito client associated with the Cognito User Pool has to
160 | support `ALLOW_ADMIN_USER_PASSWORD_AUTH` and that the AWS account has to
161 | have permissions necessary to call Cognito's
162 | [AdminInitiateAuth API][AdminInitiateAuth] and [AdminGetUser API][AdminGetUser].
163 |
164 | See [Amazon Cognito and Latest OAuth/OIDC Specifications][CognitoTutorial]
165 | for details.
166 |
167 | See Also
168 | --------
169 |
170 | - [Authlete][Authlete] - Authlete Home Page
171 | - [authlete-python][AuthletePython] - Authlete Library for Python
172 | - [authlete-python-django][AuthletePythonDjango] - Authlete Library for Django (Python)
173 | - [django-resource-server][DjangoResourceServer] - Resource Server Implementation
174 |
175 | Contact
176 | -------
177 |
178 | Contact Form : https://www.authlete.com/contact/
179 |
180 | | Purpose | Email Address |
181 | |:----------|:---------------------|
182 | | General | info@authlete.com |
183 | | Sales | sales@authlete.com |
184 | | PR | pr@authlete.com |
185 | | Technical | support@authlete.com |
186 |
187 | [AdminGetUser]: https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminGetUser.html
188 | [AdminInitiateAuth]: https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminInitiateAuth.html
189 | [Authlete]: https://www.authlete.com/
190 | [AuthleteAPI]: https://docs.authlete.com/
191 | [AuthleteGettingStarted]: https://www.authlete.com/developers/getting_started/
192 | [AuthleteOverview]: https://www.authlete.com/developers/overview/
193 | [AuthletePython]: https://github.com/authlete/authlete-python/
194 | [AuthletePythonDjango]: https://github.com/authlete/authlete-python-django/
195 | [AuthleteSignUp]: https://so.authlete.com/accounts/signup
196 | [Boto3]: https://boto3.amazonaws.com/v1/documentation/api/latest/index.html
197 | [Cognito]: https://aws.amazon.com/cognito/
198 | [CognitoTutorial]: https://www.authlete.com/developers/tutorial/cognito/
199 | [DeveloperConsole]: https://www.authlete.com/developers/cd_console/
200 | [Django]: https://www.djangoproject.com/
201 | [DjangoOAuthServer]: https://github.com/authlete/django-oauth-server/
202 | [DjangoResourceServer]: https://github.com/authlete/django-resource-server/
203 | [ImplicitFlow]: https://tools.ietf.org/html/rfc6749#section-4.2
204 | [MultiResponseType]: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
205 | [OIDC]: https://openid.net/connect/
206 | [OIDCCore]: https://openid.net/specs/openid-connect-core-1_0.html
207 | [OIDCDiscovery]: https://openid.net/specs/openid-connect-discovery-1_0.html
208 | [PKCE]: https://www.authlete.com/developers/pkce/
209 | [RFC6749]: https://tools.ietf.org/html/rfc6749
210 | [RFC7009]: https://tools.ietf.org/html/rfc7009
211 | [RFC7636]: https://tools.ietf.org/html/rfc7636
212 | [RFC7662]: https://tools.ietf.org/html/rfc7662
213 | [UserInfoEndpoint]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
214 |
--------------------------------------------------------------------------------
/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/authlete/django-oauth-server/327bf0a26f9cc73a62acaefef9921a8646827233/api/__init__.py
--------------------------------------------------------------------------------
/api/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/api/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ApiConfig(AppConfig):
5 | name = 'api'
6 |
--------------------------------------------------------------------------------
/api/authorization_decision_endpoint.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | import logging
19 | from django.contrib.auth import authenticate, login
20 | from authlete.django.handler.authorization_request_decision_handler import AuthorizationRequestDecisionHandler
21 | from .base_endpoint import BaseEndpoint
22 | from .spi.authorization_request_decision_handler_spi_impl import AuthorizationRequestDecisionHandlerSpiImpl
23 |
24 |
25 | logger = logging.getLogger(__name__)
26 |
27 |
28 | class AuthorizationDecisionEndpoint(BaseEndpoint):
29 | def __init__(self, api):
30 | super().__init__(api)
31 |
32 |
33 | def handle(self, request):
34 | # Authenticate the user if necessary.
35 | self.__authenticateUserIfNecessary(request)
36 |
37 | # Flag which indicates whether the user has given authorization
38 | # to the client application or not.
39 | authorized = self.__isClientAuthorized(request)
40 |
41 | # Process the authorization request according to the user's decision.
42 | return self.__handleDecision(request, authorized)
43 |
44 |
45 | def __authenticateUserIfNecessary(self, request):
46 | if request.user.is_authenticated:
47 | # The user has already logged in.
48 | return
49 |
50 | loginId = request.POST.get('loginId')
51 | password = request.POST.get('password')
52 |
53 | # Authenticate the user.
54 | user = authenticate(username=loginId, password=password)
55 | if user is None:
56 | # User authentication failed.
57 | logger.debug("authorization_decision_endpoint: User authentication failed. The presented login ID is {}.".format(loginId))
58 | return
59 |
60 | logger.debug("authorization_decision_endpoint: User authentication succeeded. The presented login ID is {}.".format(loginId))
61 |
62 | # Let the user log in.
63 | login(request, user)
64 |
65 |
66 | def __isClientAuthorized(self, request):
67 | # If the user pressed the "Authorize" button, the request contains
68 | # an "authorized" parameter.
69 | return ('authorized' in request.POST)
70 |
71 |
72 | def __handleDecision(self, request, authorized):
73 | spi = AuthorizationRequestDecisionHandlerSpiImpl(request, authorized)
74 | handler = AuthorizationRequestDecisionHandler(self.api, spi)
75 |
76 | # Parameters contained in the response from /api/auth/authorization API.
77 | session = request.session
78 | ticket = session.get('ticket')
79 | claimNames = session.get('claimNames')
80 | claimLocales = session.get('claimLocales')
81 |
82 | return handler.handle(ticket, claimNames, claimLocales)
83 |
--------------------------------------------------------------------------------
/api/authorization_endpoint.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019-2021 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | import logging
19 | import time
20 | from django.contrib.auth import logout
21 | from django.contrib.auth.models import User
22 | from django.shortcuts import render
23 | from authlete.django.handler.authorization_request_base_handler import AuthorizationRequestBaseHandler
24 | from authlete.django.handler.authorization_request_error_handler import AuthorizationRequestErrorHandler
25 | from authlete.django.handler.no_interaction_handler import NoInteractionHandler
26 | from authlete.django.web.request_utility import RequestUtility
27 | from authlete.dto.authorization_action import AuthorizationAction
28 | from authlete.dto.authorization_fail_action import AuthorizationFailAction
29 | from authlete.dto.authorization_fail_reason import AuthorizationFailReason
30 | from authlete.dto.authorization_fail_request import AuthorizationFailRequest
31 | from authlete.dto.authorization_request import AuthorizationRequest
32 | from authlete.types.prompt import Prompt
33 | from .authorization_page_model import AuthorizationPageModel
34 | from .base_endpoint import BaseEndpoint
35 | from .spi.no_interaction_handler_spi_impl import NoInteractionHandlerSpiImpl
36 |
37 |
38 | logger = logging.getLogger(__name__)
39 |
40 |
41 | class AuthorizationEndpoint(BaseEndpoint):
42 | def __init__(self, api):
43 | super().__init__(api)
44 |
45 |
46 | def handle(self, request):
47 | # Query parameters or form parameters. OIDC Core 1.0 requires that
48 | # the authorization endpoint support both GET and POST methods.
49 | params = RequestUtility.extractParameters(request)
50 |
51 | # Call Authlete's /api/auth/authorization API.
52 | res = self.__callAuthorizationApi(params)
53 |
54 | # 'action' in the response denotes the next action which this
55 | # authorization endpoint implementation should take.
56 | action = res.action
57 |
58 | if action == AuthorizationAction.INTERACTION:
59 | # Process the authorization request with user interaction.
60 | return self.__handleInteraction(request, res)
61 | elif action == AuthorizationAction.NO_INTERACTION:
62 | # Process the authorization request without user interaction.
63 | # The flow reaches here only when the authorization request
64 | # contains 'prompt=none'.
65 | return self.__handleNoInteraction(request, res)
66 | else:
67 | # Handle other error cases.
68 | return self.__handleError(res)
69 |
70 |
71 | def __callAuthorizationApi(self, parameters):
72 | # Create a request for /api/auth/authorization API.
73 | req = AuthorizationRequest()
74 | req.parameters = parameters
75 |
76 | # Call /api/auth/authorization API.
77 | return self.api.authorization(req)
78 |
79 |
80 | def __handleNoInteraction(self, request, response):
81 | logger.debug("authorization_endpoint: Processing the request without user interaction.")
82 |
83 | # Make NoInteractionHandler handle the case of 'prompt=none'.
84 | # An implementation of the NoInteractionHandlerSpi interface
85 | # needs to be given to the constructor of NoInteractionHandler.
86 | return NoInteractionHandler(
87 | self.api, NoInteractionHandlerSpiImpl(request)).handle(response)
88 |
89 |
90 | def __handleError(self, response):
91 | logger.debug("authorization_endpoint: The request caused an error: {}".format(response.resultMessage))
92 |
93 | # Make AuthorizationRequestErrorHandler handle the error case.
94 | return AuthorizationRequestErrorHandler().handle(response)
95 |
96 |
97 | def __handleInteraction(self, request, response):
98 | logger.debug("authorization_endpoint: Processing the request with user interaction.")
99 |
100 | # Prepare a model object which is needed to render the authorization page.
101 | model = self.__prepareModel(request, response)
102 |
103 | # In the current implementation, model is None only when there is no user
104 | # who has the required subject.
105 | if model is None:
106 | return self.__authorizationFail(
107 | response.ticket, AuthorizationFailReason.NOT_AUTHENTICATED)
108 |
109 | # Store some variables into the session so that they can be
110 | # referred to later in authorization_decision_endpoint.py.
111 | session = request.session
112 | session['ticket'] = response.ticket
113 | session['claimNames'] = response.claims
114 | session['claimLocales'] = response.claimsLocales
115 |
116 | # Render the authorization page.
117 | return render(request, 'api/authorization.html', {'model':model})
118 |
119 |
120 | def __prepareModel(self, request, response):
121 | # Model object used to render the authorization page.
122 | model = AuthorizationPageModel(response)
123 |
124 | # Check if login is required.
125 | model.loginRequired = self.__isLoginRequired(request, response)
126 |
127 | if model.loginRequired == False:
128 | # The user's name that will be referred to in the authorization page.
129 | model.userName = request.user.first_name or request.user.username
130 | return model
131 |
132 | # Logout the user (if a user has logged in).
133 | logout(request)
134 |
135 | # If the authorization request does not require a specific 'subject'.
136 | if response.subject is None:
137 | # This simple implementation uses 'login_hint' as the initial
138 | # value of the login ID.
139 | if response.loginHint is not None:
140 | model.loginId = response.loginHint
141 | return model
142 |
143 | # The authorization request requires a specific 'subject' be used.
144 |
145 | try:
146 | # Find the user whose subject is the required subject.
147 | user = User.objects.get(id=response.subject)
148 | except Exception:
149 | # There is no user who has the required subject.
150 | logger.debug("authorization_endpoint: The request fails because there is no user who has the required subject.")
151 | return None
152 |
153 | # The user who is identified by the subject exists.
154 | model.loginId = user.username
155 | model.loginIdReadOnly = 'readonly'
156 |
157 | return model
158 |
159 |
160 | def __isLoginRequired(self, request, response):
161 | # If no user has logged in.
162 | if request.user.is_authenticated == False:
163 | return True
164 |
165 | # Check if the 'prompt' parameter includes 'login'.
166 | included = self.__isLoginIncludedInPrompt(response)
167 | if included:
168 | # Login is explicitly required by the client.
169 | # The user has to re-login.
170 | logger.debug("authorization_endpoint: Login is required because 'prompt' includes 'login'.")
171 | return True
172 |
173 | # If the authorization request requires a subject.
174 | if response.subject is not None:
175 | # If the current user's subject does not match the required one.
176 | if request.user.id != response.subject:
177 | # The user needs to login with another user account.
178 | logger.debug("authorization_endpoint: Login is required because the current user's subject does not match the required one.")
179 | return True
180 |
181 | # Check if the max age has passed since the last time the user logged in.
182 | exceeded = self.__isMaxAgeExceeded(request, response)
183 | if exceeded:
184 | # The user has to re-login.
185 | logger.debug("authorization_endpoint: Login is required because the max age has passed since the last login.")
186 | return True
187 |
188 | # Login is not required.
189 | return False
190 |
191 |
192 | def __isLoginIncludedInPrompt(self, response):
193 | # If the authorization request does not include a 'prompt' parameter.
194 | if response.prompts is None:
195 | return False
196 |
197 | # For each value in the 'prompt' parameter.
198 | for prompt in response.prompts:
199 | if prompt == Prompt.LOGIN:
200 | # 'login' is included in the 'prompt' parameter.
201 | return True
202 |
203 | # The 'prompt' parameter does not include 'login'.
204 | return False
205 |
206 |
207 | def __isMaxAgeExceeded(self, request, response):
208 | # If the authorization request does not include a 'max_age' parameter
209 | # and the 'default_max_age' metadata of the client is not set.
210 | if response.maxAge <= 0:
211 | # Don't have to care about the maximum authentication age.
212 | return False
213 |
214 | # Calculate the number of seconds that have elapsed since the last login.
215 | age = int(time.time() - request.user.last_login)
216 |
217 | if age <= response.maxAge:
218 | # The max age is not exceeded yet.
219 | return False
220 |
221 | # The max age has been exceeded.
222 | return True
223 |
224 |
225 | def __authorizationFail(self, ticket, reason):
226 | # Call /api/auth/authorization/fail API.
227 | handler = AuthorizationRequestBaseHandler(self.api)
228 | return handler.authorizationFail(ticket, reason)
229 |
--------------------------------------------------------------------------------
/api/authorization_page_model.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | class AuthorizationPageModel(object):
19 | def __init__(self, response):
20 | client = response.client
21 |
22 | self._serviceName = response.service.serviceName
23 | self._clientName = client.clientName
24 | self._description = client.description
25 | self._logoUri = client.logoUri
26 | self._clientUri = client.clientUri
27 | self._policyUri = client.policyUri
28 | self._tosUri = client.tosUri
29 | self._scopes = response.scopes
30 | self._loginId = ''
31 | self._loginIdReadOnly = ''
32 | self._userName = None
33 |
34 |
35 | @property
36 | def serviceName(self):
37 | return self._serviceName
38 |
39 |
40 | @property
41 | def clientName(self):
42 | return self._clientName
43 |
44 |
45 | @property
46 | def description(self):
47 | return self._description
48 |
49 |
50 | @property
51 | def logoUri(self):
52 | return self._logoUri
53 |
54 |
55 | @property
56 | def clientUri(self):
57 | return self._clientUri
58 |
59 |
60 | @property
61 | def policyUri(self):
62 | return self._policyUri
63 |
64 |
65 | @property
66 | def tosUri(self):
67 | return self._tosUri
68 |
69 |
70 | @property
71 | def scopes(self):
72 | return self._scopes
73 |
74 |
75 | @property
76 | def loginId(self):
77 | return self._loginId
78 |
79 |
80 | @loginId.setter
81 | def loginId(self, value):
82 | self._loginId = value
83 |
84 |
85 | @property
86 | def loginIdReadOnly(self):
87 | return self._loginIdReadOnly
88 |
89 |
90 | @loginIdReadOnly.setter
91 | def loginIdReadOnly(self, value):
92 | self._loginIdReadOnly = value
93 |
94 |
95 | @property
96 | def loginRequired(self):
97 | return self._loginRequired
98 |
99 |
100 | @loginRequired.setter
101 | def loginRequired(self, value):
102 | self._loginRequired = value
103 |
104 |
105 | @property
106 | def userName(self):
107 | return self._userName
108 |
109 |
110 | @userName.setter
111 | def userName(self, value):
112 | self._userName = value
113 |
--------------------------------------------------------------------------------
/api/base_endpoint.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | class BaseEndpoint(object):
19 | def __init__(self, api):
20 | super().__init__()
21 | self._api = api
22 |
23 |
24 | @property
25 | def api(self):
26 | return self._api
27 |
--------------------------------------------------------------------------------
/api/introspection_endpoint.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | from authlete.django.handler.introspection_request_handler import IntrospectionRequestHandler
19 | from authlete.django.web.basic_credentials import BasicCredentials
20 | from authlete.django.web.response_utility import ResponseUtility
21 | from .base_endpoint import BaseEndpoint
22 |
23 |
24 | class IntrospectionEndpoint(BaseEndpoint):
25 | def __init__(self, api):
26 | super().__init__(api)
27 |
28 |
29 | def handle(self, request):
30 | # "1.1. Introspection Request" in RFC 7662 says as follows:
31 | #
32 | # To prevent token scanning attacks, the endpoint MUST also require
33 | # some form of authorization to access this endpoint, such as client
34 | # authentication as described in OAuth 2.0 [RFC6749] or a separate
35 | # OAuth 2.0 access token such as the bearer token described in OAuth
36 | # 2.0 Bearer Token Usage [RFC6750]. The methods of managing and
37 | # validating these authentication credentials are out of scope of
38 | # this specification.
39 | #
40 | # Therefore, this API must be protected in some way or other. Let's
41 | # perform authentication of the API caller.
42 | authenticated = self.__authenticate_api_caller(request)
43 |
44 | # If the API caller does not have necessary privilages to call this API.
45 | if authenticated == False:
46 | # 401 Unauthorized
47 | return ResponseUtility.unauthorized('Basic realm="/api/introspection"')
48 |
49 | # Call Authlete's /api/auth/introspection/standard API.
50 | return IntrospectionRequestHandler(self.api).handle(request)
51 |
52 |
53 | def __authenticate_api_caller(self, request):
54 | # NOTE: THIS IMPLEMENTATION IS FOR DEMONSTRATION PURPOSES ONLY.
55 |
56 | # Get the value of the Authorization header.
57 | auth = request.headers.get('Authorization')
58 |
59 | # Try to parse it as "Basic Authentication"
60 | credentials = BasicCredentials.parse(auth)
61 |
62 | # If the Authorization header does not contain "Basic Authentication"
63 | # or the user ID is not valid.
64 | if credentials.userId is None:
65 | # Authentication of the API caller failed.
66 | return False
67 |
68 | # If the user ID is "nobody"
69 | if credentials.userId == 'nobody':
70 | # Reject the introspection request from "nobody".
71 | return False
72 |
73 | # Accept anybody except "nobody" regardless of whatever the value of
74 | # credentials.password is.
75 | return True
76 |
--------------------------------------------------------------------------------
/api/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/authlete/django-oauth-server/327bf0a26f9cc73a62acaefef9921a8646827233/api/migrations/__init__.py
--------------------------------------------------------------------------------
/api/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/api/spi/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | from .authorization_request_decision_handler_spi_impl import AuthorizationRequestDecisionHandlerSpiImpl
19 | from .authorization_request_handler_spi_impl import AuthorizationRequestHandlerSpiImpl
20 | from .no_interaction_handler_spi_impl import NoInteractionHandlerSpiImpl
21 | from .token_request_handler_spi_impl import TokenRequestHandlerSpiImpl
22 |
--------------------------------------------------------------------------------
/api/spi/authorization_request_decision_handler_spi_impl.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | from authlete.django.handler.spi.authorization_request_decision_handler_spi import AuthorizationRequestDecisionHandlerSpi
19 | from .authorization_request_handler_spi_impl import AuthorizationRequestHandlerSpiImpl
20 |
21 |
22 | class AuthorizationRequestDecisionHandlerSpiImpl(
23 | AuthorizationRequestHandlerSpiImpl, AuthorizationRequestDecisionHandlerSpi):
24 | def __init__(self, request, clientAuthorized):
25 | super().__init__(request)
26 | self._clientAuthorized = clientAuthorized
27 |
28 |
29 | def isClientAuthorized(self):
30 | return self._clientAuthorized
31 |
--------------------------------------------------------------------------------
/api/spi/authorization_request_handler_spi_impl.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | from django.contrib.auth.models import User
19 | from authlete.django.handler.spi.authorization_request_handler_spi_adapter import AuthorizationRequestHandlerSpiAdapter
20 | from authlete.types.standard_claims import StandardClaims
21 |
22 |
23 | class AuthorizationRequestHandlerSpiImpl(AuthorizationRequestHandlerSpiAdapter):
24 | def __init__(self, request):
25 | self._user = None
26 | self._tried = False
27 | self._request = request
28 |
29 |
30 | def getUserClaimValue(self, subject, claimName, languageTag):
31 | # The user identified by the subject.
32 | user = self.__getUser(subject)
33 | if user is None:
34 | return None
35 |
36 | if claimName == StandardClaims.NAME:
37 | if user.first_name and user.last_name:
38 | return '{} {}'.format(user.first_name, user.last_name)
39 | elif claimName == StandardClaims.GIVEN_NAME:
40 | if user.first_name:
41 | return user.first_name
42 | elif claimName == StandardClaims.FAMILY_NAME:
43 | if user.last_name:
44 | return user.last_name
45 | elif claimName == StandardClaims.EMAIL:
46 | if user.email:
47 | return user.email
48 | else:
49 | return None
50 |
51 | return None
52 |
53 |
54 | def __getUser(self, subject):
55 | if self._tried == False:
56 | try:
57 | self._user = User.objects.get(id=subject)
58 | except:
59 | self._user = None
60 |
61 | self._tried = True
62 |
63 | return self._user
64 |
65 |
66 | def getUserAuthenticatedAt(self):
67 | user = self._request.user
68 | if user.is_authenticated == False:
69 | return 0
70 |
71 | authenticatedAt = user.last_login
72 | if authenticatedAt is None:
73 | return 0
74 |
75 | return int(authenticatedAt.timestamp())
76 |
77 |
78 | def getUserSubject(self):
79 | user = self._request.user
80 | if user.is_authenticated == False:
81 | return None
82 |
83 | return user.id
84 |
--------------------------------------------------------------------------------
/api/spi/no_interaction_handler_spi_impl.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | from authlete.django.handler.spi.no_interaction_handler_spi import NoInteractionHandlerSpi
19 | from .authorization_request_handler_spi_impl import AuthorizationRequestHandlerSpiImpl
20 |
21 |
22 | class NoInteractionHandlerSpiImpl(AuthorizationRequestHandlerSpiImpl, NoInteractionHandlerSpi):
23 | def __init__(self, request):
24 | super().__init__(request)
25 |
26 |
27 | def isUserAuthenticated(self):
28 | return (self.getUserSubject() is not None)
29 |
--------------------------------------------------------------------------------
/api/spi/token_request_handler_spi_impl.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2019 Authlete, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing,
11 | # software distributed under the License is distributed on an
12 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | # either express or implied. See the License for the specific
14 | # language governing permissions and limitations under the
15 | # License.
16 |
17 |
18 | from django.contrib.auth import authenticate
19 | from authlete.django.handler.spi.token_request_handler_spi_adapter import TokenRequestHandlerSpiAdapter
20 |
21 |
22 | class TokenRequestHandlerSpiImpl(TokenRequestHandlerSpiAdapter):
23 | def authenticateUser(self, username, password):
24 | # NOTE:
25 | # This method needs to be implemented only when you want to support
26 | # "Resource Owner Password Credentials" flow (RFC 6749, 4.3.)
27 |
28 | # Authenticate the user with the given credentials.
29 | user = authenticate(username=username, password=password)
30 |
31 | # If the user is not found.
32 | if user is None or user.is_active == False:
33 | # There is no user who has the credentials.
34 | return None
35 |
36 | # Return the subject (= unique identifier) of the user.
37 | return user.id
38 |
--------------------------------------------------------------------------------
/api/static/api/css/authorization.css:
--------------------------------------------------------------------------------
1 | .font-default
2 | {
3 | font-family: 'Source Sans Pro', 'Helvetica Neue', 'Segoe UI', 'Arial', sans-serif;
4 | -webkit-font-smoothing: antialiased;
5 | color: #666;
6 | }
7 |
8 | body {
9 | margin: 0;
10 | text-shadow: none;
11 | }
12 |
13 | p {
14 | margin-top: 0;
15 | }
16 |
17 | h3, h4 {
18 | color: steelblue;
19 | }
20 |
21 | .indent {
22 | margin-left: 15px;
23 | }
24 |
25 | #page_title {
26 | background: #F5F5F5;
27 | color: steelblue;
28 | padding: 0.5em;
29 | margin: 0;
30 | }
31 |
32 | #content {
33 | padding: 0 20px 20px;
34 | }
35 |
36 | #logo {
37 | width: 150px;
38 | height: 150px;
39 | background: lightgray;
40 | margin: 0 20px 10px 5px;
41 | float: left;
42 | }
43 |
44 | #client-summary {
45 | float: left;
46 | }
47 |
48 | #client-link-list {
49 | margin: 0;
50 | padding: 0;
51 | }
52 |
53 | #client-link-list li {
54 | list-style-type: none;
55 | }
56 |
57 | #client-link-list a {
58 | position: relative;
59 | padding-left: 25px;
60 | text-decoration: none;
61 | color: cadetblue;
62 | }
63 |
64 | #client-link-list a:hover {
65 | text-decoration: underline;
66 | }
67 |
68 | #client-link-list a:before {
69 | display: block;
70 | content: "";
71 | position: absolute;
72 | top: 50%;
73 | left: 0;
74 | width: 0;
75 | margin: -5px 0 0 0;
76 | border-top: 12px solid cadetblue;
77 | border-left: 12px solid transparent;
78 | -webkit-transform: rotate(45deg);
79 | transform: rotate(45deg);
80 | }
81 |
82 | #scope-list {
83 | margin-left: 20px;
84 | }
85 |
86 | #scope-list dt {
87 | font-weight: bold;
88 | }
89 |
90 | #scope-list dd {
91 | margin-bottom: 10px;
92 | }
93 |
94 | input {
95 | color: black;
96 | }
97 |
98 | #login-fields {
99 | margin-bottom: 20px;
100 | }
101 |
102 | #login-prompt {
103 | font-size: 85%;
104 | margin-bottom: 5px;
105 | }
106 |
107 | #loginId {
108 | display: block;
109 | border: 1px solid #666;
110 | border-bottom: none;
111 | padding: 0.3em 0.5em;
112 | width: 300px;
113 | }
114 |
115 | #password {
116 | display: block;
117 | border: 1px solid #666;
118 | padding: 0.3em 0.5em;
119 | width: 300px;
120 | }
121 |
122 | #authorization-form-buttons {
123 | margin: 20px auto;
124 | }
125 |
126 | #authorize-button, #deny-button {
127 | display: inline-block;
128 | width: 150px;
129 | padding: 12px 0;
130 | margin: 13px;
131 | min-height: 26px;
132 | text-align: center;
133 | text-decoration: none;
134 | outline: 0;
135 | -webkit-transition: none;
136 | transition: none;
137 | }
138 |
139 | #authorize-button {
140 | background-color: #4285f4;
141 | color: white;
142 | }
143 |
144 | #authorize-button:hover {
145 | background-color: #1255f4;
146 | }
147 |
148 | #authorize-button:active {
149 | background-color: blue;
150 | }
151 |
152 | #deny-button {
153 | background-color: #f08080;
154 | color: white;
155 | }
156 |
157 | #deny-button:hover {
158 | background-color: #f05050;
159 | }
160 |
161 | #deny-button:active {
162 | background-color: red;
163 | }
164 |
--------------------------------------------------------------------------------
/api/templates/api/authorization.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |