├── .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 | 4 | 5 | 6 | {{ model.serviceName }} | Authorization Page 7 | 8 | 9 | 10 |
{{ model.serviceName }}
11 | 12 |
13 |

{{ model.clientName }}

14 |
15 | 16 | 17 |
18 |

{{ model.description }}

19 | 30 |
31 | 32 |
33 |
34 | 35 | {% if model.scopes is not None %} 36 |

Permissions

37 |
38 |

The application is requesting the following permissions.

39 |
40 | {% for scope in model.scopes %} 41 |
{{ scope.name }}
42 |
{{ scope.description }}
43 | {% endfor %} 44 |
45 |
46 | {% endif %} 47 | 48 |

Authorization

49 |
50 | {% if model.userName is not None %} 51 |

Hello {{ model.userName }},

52 | {% endif %} 53 |

Do you grant authorization to the application?

54 | 55 |
56 | {% csrf_token %} 57 | {% if model.loginRequired %} 58 |
59 |
Input Login ID and password.
60 | 63 | 65 |
66 | {% endif %} 67 |
68 | 69 | 70 |
71 |
72 |
73 |
74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /api/urls.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.urls import path 19 | from . import views 20 | 21 | app_name = 'api' 22 | 23 | urlpatterns = [ 24 | path('authorization', views.authorization), 25 | path('authorization/decision', views.authorization_decision, name='authorization_decision'), 26 | path('jwks', views.jwks), 27 | path('introspection', views.introspection), 28 | path('revocation', views.revocation), 29 | path('token', views.token), 30 | ] 31 | -------------------------------------------------------------------------------- /api/views.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019-2024 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.conf import settings 19 | from django.views.decorators.csrf import csrf_exempt 20 | from django.views.decorators.http import require_GET, require_POST, require_http_methods 21 | from authlete.django.handler import * 22 | from authlete.dto import * 23 | from .authorization_decision_endpoint import AuthorizationDecisionEndpoint 24 | from .authorization_endpoint import AuthorizationEndpoint 25 | from .introspection_endpoint import IntrospectionEndpoint 26 | from .spi.token_request_handler_spi_impl import TokenRequestHandlerSpiImpl 27 | 28 | 29 | @require_http_methods(['GET', 'POST']) 30 | def authorization(request): 31 | """Authorization Endpoint""" 32 | return AuthorizationEndpoint(settings.AUTHLETE_API).handle(request) 33 | 34 | 35 | @require_POST 36 | def authorization_decision(request): 37 | """Authorization Decision Endpoint""" 38 | return AuthorizationDecisionEndpoint(settings.AUTHLETE_API).handle(request) 39 | 40 | 41 | @require_GET 42 | def configuration(request): 43 | """Discovery Endpoint (.well-known/openid-configuration)""" 44 | return ConfigurationRequestHandler(settings.AUTHLETE_API).handle(request) 45 | 46 | 47 | @require_GET 48 | def federation_configuration(request): 49 | """Federation Configuration Endpoint (.well-known/openid-federation)""" 50 | req = FederationConfigurationRequest() 51 | req.entityTypes = ['OPENID_PROVIDER', 'OPENID_CREDENTIAL_ISSUER'] 52 | 53 | return FederationConfigurationRequestHandler(settings.AUTHLETE_API).handle(req) 54 | 55 | 56 | @require_POST 57 | @csrf_exempt 58 | def introspection(request): 59 | """Introspection Endpoint""" 60 | return IntrospectionEndpoint(settings.AUTHLETE_API).handle(request) 61 | 62 | 63 | @require_GET 64 | def jwks(request): 65 | """JWK Set Endpoint""" 66 | return JwksRequestHandler(settings.AUTHLETE_API).handle(request) 67 | 68 | 69 | @require_POST 70 | @csrf_exempt 71 | def revocation(request): 72 | """Revocation Endpoint""" 73 | return RevocationRequestHandler(settings.AUTHLETE_API).handle(request) 74 | 75 | 76 | @require_POST 77 | @csrf_exempt 78 | def token(request): 79 | """Token Endpoint""" 80 | return TokenRequestHandler( 81 | settings.AUTHLETE_API, TokenRequestHandlerSpiImpl()).handle(request) 82 | -------------------------------------------------------------------------------- /authlete.ini: -------------------------------------------------------------------------------- 1 | [authlete] 2 | ;------------------------------------------------------------ 3 | ; For Authlete 2.x 4 | ;------------------------------------------------------------ 5 | base_url = https://api.authlete.com 6 | service.api_key = YOUR_SERVICE_API_KEY 7 | service.api_secret = YOUR_SERVICE_API_SECRET 8 | 9 | ;------------------------------------------------------------ 10 | ; For Authlete 3.x 11 | ;------------------------------------------------------------ 12 | ;api_version = V3 13 | ;base_url = https://nextdev-api.authlete.net 14 | ;service.api_key = YOUR_SERVICE_API_KEY 15 | ;service.access_token = YOUR_ACCESS_TOKEN 16 | -------------------------------------------------------------------------------- /backends/__init__.py: -------------------------------------------------------------------------------- 1 | from .cognito_backend import CognitoBackend 2 | -------------------------------------------------------------------------------- /backends/cognito_backend.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 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 | # This class is an implementation of Django's authentication backends. 19 | # This implementation delegates user authentication to Amazon Cognito. 20 | # 21 | # A Django authentication backend has to implement two methods; 22 | # `authenticate(request, **credentials)` and `get_user(user_id)`. 23 | # This class implements the two methods by using Cognito's 24 | # `AdminInitiateAuth` API and `AdminGetUser` API, respectively. 25 | # 26 | # References: 27 | # 28 | # Customizing authentication in Django 29 | # https://docs.djangoproject.com/en/3.1/topics/auth/customizing/ 30 | # 31 | # AWS SDK for Python (Boto3) / CognitoIdentityProvider 32 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html 33 | # 34 | # AdminInitiateAuth API 35 | # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminInitiateAuth.html 36 | # 37 | # AdminGetUser API 38 | # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminGetUser.html 39 | # 40 | 41 | 42 | import boto3 43 | import logging 44 | from django.conf import settings 45 | from django.contrib.auth.backends import BaseBackend 46 | from django.contrib.auth.models import User 47 | from authlete.types.standard_claims import StandardClaims 48 | 49 | 50 | logger = logging.getLogger(__name__) 51 | 52 | 53 | class CognitoBackend(BaseBackend): 54 | def __init__(self): 55 | # Create an instance to access Cognito APIs. 56 | self._cognito_idp = boto3.client('cognito-idp') 57 | 58 | 59 | def authenticate(self, request, username=None, password=None): 60 | # Call Cognito's AdminInitiateAuth API. 61 | response = self.__cognito_admin_initiate_auth(username, password) 62 | if response is None: 63 | # The user was not authenticated. 64 | return None 65 | 66 | # Build a User object for the authenticated user. 67 | return self.get_user(username) 68 | 69 | 70 | def get_user(self, user_id): 71 | # Call Cognito's AdminGetUser API. 72 | response = self.__cognito_admin_get_user(user_id) 73 | if response is None: 74 | # Information about the user was not available. 75 | return None 76 | 77 | # Build a User object based on the information in the response. 78 | return self.__build_user(user_id, response) 79 | 80 | 81 | def __cognito_admin_initiate_auth(self, username, password): 82 | # If settings for Cognito are not available. 83 | if not settings.COGNITO_USER_POOL_ID: 84 | return None 85 | 86 | try: 87 | # Call Cognito's AdminInitiateAuth API. 88 | return self.__call_cognito_admin_initiate_auth(username, password) 89 | except self._cognito_idp.exceptions.NotAuthorizedException: 90 | # There is no user who has the username and the password. 91 | logger.debug("Cognito user authentication for '%s' failed.", username) 92 | return None 93 | except Exception: 94 | # Something wrong happened in calling Cognito AdminInitiateAuth API. 95 | logger.error("Cognito AdminInitiateAuth API failed.", exc_info=True) 96 | return None 97 | 98 | 99 | def __call_cognito_admin_initiate_auth(self, username, password): 100 | # Call Cognito's AdminInitiateAuth API. 101 | return self._cognito_idp.admin_initiate_auth( 102 | UserPoolId = settings.COGNITO_USER_POOL_ID, 103 | ClientId = settings.COGNITO_CLIENT_ID, 104 | AuthFlow = 'ADMIN_USER_PASSWORD_AUTH', 105 | AuthParameters = { 106 | 'USERNAME' : username, 107 | 'PASSWORD' : password 108 | } 109 | ) 110 | 111 | 112 | def __cognito_admin_get_user(self, username): 113 | # If settings for Cognito are not available. 114 | if not settings.COGNITO_USER_POOL_ID: 115 | return None 116 | 117 | try: 118 | # Call Cognito's AdminGetUser API. 119 | return self.__call_cognito_admin_get_user(username) 120 | except self._cognito_idp.exceptions.UserNotFoundException: 121 | # The user was not found in the Cognito User Pool. 122 | logger.debug("The user '%s' was not found in the Cognito User Pool.", username) 123 | return None 124 | except Exception: 125 | # Something wrong happened in calling Cognito AdminGetUser API. 126 | logger.error("Cognito AdminGetUser API failed.", exc_info=True) 127 | return None 128 | 129 | 130 | def __call_cognito_admin_get_user(self, username): 131 | # Call Cognito's AdminGetUser API. 132 | return self._cognito_idp.admin_get_user( 133 | UserPoolId = settings.COGNITO_USER_POOL_ID, 134 | Username = username 135 | ) 136 | 137 | 138 | def __build_user(self, user_id, response): 139 | try: 140 | # Search the list of Django User objects for the user. 141 | user = User.objects.get(username = user_id) 142 | except User.DoesNotExist: 143 | # Create a new Django User object for the user. 144 | user = User.objects.create_user(user_id) 145 | 146 | # If the response from Cognito's AdminGetUser does not contain 147 | # 'UserAttributes' 148 | if 'UserAttributes' not in response: 149 | return user 150 | 151 | for attribute in response['UserAttributes']: 152 | name = attribute['Name'] 153 | value = attribute['Value'] 154 | 155 | # Cognito User Pool supports most of standard claims defined in 156 | # "OpenID Connect Core 1.0 Section 5.1. Standard Claims". However, 157 | # the default User object of Django does not. If you want to 158 | # support more claims, you have to customize the User object and 159 | # then add more 'elif' here and getUserClaimValue() method in 160 | # AuthorizationRequestHandlerSpiImpl class. 161 | if name == StandardClaims.EMAIL: 162 | user.email = value 163 | elif name == StandardClaims.GIVEN_NAME: 164 | user.first_name = value 165 | elif name == StandardClaims.FAMILY_NAME: 166 | user.last_name = value 167 | 168 | user.save() 169 | 170 | return user 171 | -------------------------------------------------------------------------------- /django_oauth_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authlete/django-oauth-server/327bf0a26f9cc73a62acaefef9921a8646827233/django_oauth_server/__init__.py -------------------------------------------------------------------------------- /django_oauth_server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_oauth_server project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'wo(@ggo#^@1=3d8#f-y$zg@&&qxgono0!-+qo2sxtne=0)a_)#' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'api.apps.ApiConfig', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'django_oauth_server.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'django_oauth_server.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | 123 | 124 | #-------------------------------------------------- 125 | # Authlete 126 | #-------------------------------------------------- 127 | from authlete.api import AuthleteApiImpl 128 | from authlete.conf import AuthleteIniConfiguration 129 | 130 | # Read Authlete settings from 'authlete.ini' and set timeouts. 131 | # See https://github.com/authlete/authlete-python/README.md for details. 132 | AUTHLETE_API = AuthleteApiImpl(AuthleteIniConfiguration()) 133 | AUTHLETE_API.getSettings().connectionTimeout = 5.0 134 | AUTHLETE_API.getSettings().readTimeout = 5.0 135 | 136 | 137 | #-------------------------------------------------- 138 | # Amazon Cognito 139 | #-------------------------------------------------- 140 | 141 | # To use Amazon Cognito as a user database, 'CognitoBackend' (which is defined 142 | # in 'cognito_backend.py') needs to be recognized as an 'authentication backend'. 143 | # See the online document below for details. 144 | # 145 | # Customizing authentication in Django / Specifying authentication backends 146 | # https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#specifying-authentication-backends 147 | # 148 | 149 | #AUTHENTICATION_BACKENDS = ('backends.CognitoBackend',) 150 | 151 | # In addition, COGNITO_USER_POOL_ID and COGNITO_CLIENT_ID have to be set up 152 | # properly. Note that ALLOW_ADMIN_USER_PASSWORD_AUTH has to be enabled for 153 | # the Cognito client application. 154 | 155 | #COGNITO_USER_POOL_ID = 'YOUR_COGNITO_USER_POOL_ID' 156 | #COGNITO_CLIENT_ID = 'YOUR_COGNITO_CLIENT_ID' 157 | 158 | # Finally, don't forget to grant necessary permissions to the AWS account so 159 | # that it can call Cognito's AdminInitiateAuth API and AdminGetUser API. 160 | -------------------------------------------------------------------------------- /django_oauth_server/urls.py: -------------------------------------------------------------------------------- 1 | """django_oauth_server URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | from api.views import configuration, federation_configuration 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('api/', include('api.urls')), 23 | path('.well-known/openid-configuration', configuration), 24 | path('.well-known/openid-federation', federation_configuration), 25 | ] 26 | -------------------------------------------------------------------------------- /django_oauth_server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_oauth_server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_oauth_server.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_oauth_server.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | --------------------------------------------------------------------------------