├── .codebuild.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── apps ├── authorize │ ├── Makefile │ ├── rebar.config │ ├── rebar.lock │ ├── src │ │ ├── authorize.app.src │ │ └── authorize.erl │ └── test │ │ ├── authorize_SUITE.erl │ │ ├── signup.json │ │ └── tests.config ├── client │ ├── Makefile │ ├── rebar.config │ ├── rebar.lock │ ├── src │ │ ├── client.app.src │ │ └── client.erl │ └── test │ │ ├── client_SUITE.erl │ │ └── tests.config ├── github │ └── src │ │ ├── github.app.src │ │ └── github.erl ├── signin │ └── src │ │ ├── signin.app.src │ │ └── signin.erl └── token │ ├── Makefile │ ├── rebar.config │ ├── rebar.lock │ ├── src │ ├── token.app.src │ └── token.erl │ └── test │ ├── tests.config │ └── token_SUITE.erl ├── buildspec.yml ├── cleanspec.yml ├── cloud ├── cdk.json ├── package-lock.json ├── package.json ├── src │ ├── app.ts │ ├── auth.ts │ ├── client.ts │ ├── permit.ts │ ├── storage.ts │ └── token.ts ├── tsconfig.json └── tslint.json ├── doc ├── install.md ├── restapi.yml └── security.md ├── js ├── account │ ├── .env │ ├── .eslintrc │ ├── Makefile │ ├── jsconfig.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── app.css │ │ ├── app.js │ │ ├── components │ │ ├── Account │ │ │ ├── Account.js │ │ │ ├── Header.js │ │ │ └── index.js │ │ ├── Issue │ │ │ ├── Issue.js │ │ │ └── index.js │ │ ├── NewApp │ │ │ ├── KeyPair │ │ │ │ ├── KeyPair.js │ │ │ │ └── index.js │ │ │ ├── NewApp.js │ │ │ ├── Registrar │ │ │ │ ├── Endpoint.js │ │ │ │ ├── Identity.js │ │ │ │ ├── Registrar.js │ │ │ │ ├── Security.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ └── Registry │ │ │ ├── Registry.js │ │ │ ├── RegistryList.js │ │ │ ├── RegistryNone.js │ │ │ └── index.js │ │ └── index.js └── signin │ ├── .env │ ├── .eslintrc │ ├── Makefile │ ├── jsconfig.json │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── env.js │ ├── favicon.ico │ └── index.html │ └── src │ ├── app.css │ ├── app.js │ ├── components │ ├── AccessKey.js │ ├── Dialog.js │ ├── GitHub.js │ ├── KeyPair.js │ ├── SecretKey.js │ ├── SecretRecover.js │ ├── SecretReset.js │ ├── SignIn.js │ └── SignUp.js │ └── index.js ├── libs └── oauth2 │ ├── Makefile │ ├── beam.mk │ ├── include │ └── oauth2.hrl │ ├── rebar.config │ ├── rebar.lock │ ├── src │ ├── oauth2.app.src │ ├── oauth2.erl │ ├── oauth2_access.erl │ ├── oauth2_authorize.erl │ ├── oauth2_client.erl │ ├── oauth2_codec.erl │ └── oauth2_email.erl │ └── test │ ├── oauth2_FIXTURES.erl │ ├── oauth2_SUITE.erl │ ├── oauth2_client_SUITE.erl │ ├── oauth2_codec_SUITE.erl │ └── tests.config └── serverless.mk /.codebuild.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "code-build/serverless", 3 | "approver": ["fogfish"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | cdk.out/ 3 | cdk.context.json 4 | log/ 5 | rebar3 6 | *.sublime-* 7 | *.dump 8 | *.beam 9 | 10 | ## 11 | ## release artifacts 12 | *.tar.gz 13 | *.bundle 14 | 15 | ## 16 | ## created dynamically by rebar3/relx 17 | ## it shall be ignored otherwise release is dirty 18 | relx.config 19 | 20 | ## 21 | ## used by ux 22 | .env.production 23 | *.map 24 | node_modules/ 25 | build/ 26 | .DS_Store 27 | 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | language: erlang 4 | otp_release: 5 | - 22.1 6 | 7 | script: 8 | - make test 9 | -------------------------------------------------------------------------------- /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 2017 Dmitry Kolesnikov 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 | .PHONY: deps 2 | 3 | SRC = \ 4 | apps/authorize \ 5 | apps/token \ 6 | apps/client \ 7 | js/signin \ 8 | js/account 9 | 10 | 11 | deps: 12 | @npm --prefix cloud install 13 | @for I in $(SRC) ; do $(MAKE) -C $$I deps ; done 14 | 15 | test: 16 | @npm --prefix cloud run lint 17 | @for I in $(SRC) ; do $(MAKE) -C $$I test ; done 18 | 19 | dist: 20 | @for I in $(SRC) ; do $(MAKE) -C $$I dist VSN=${BUILD_RELEASE}; done 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth2 2 | 3 | The microservice based implementation of OAuth 2.0 Authorization Framework, [RFC 6749](https://tools.ietf.org/html/rfc6749). It provides an out-of-the-box, cross-platform solution for identity management. This appliance implements an automated and immutable deployment of OAuth 2.0 framework and its cloud resources using Infrastructure-as-a-Code. 4 | 5 | [![Build Status](https://secure.travis-ci.org/fogfish/oauth2.svg?branch=master)](http://travis-ci.org/fogfish/oauth2) [![GitHub release](https://img.shields.io/github/release/fogfish/oauth2.svg)](https://github.com/fogfish/oauth2/releases/latest) [![Coverage Status](https://coveralls.io/repos/github/fogfish/oauth2/badge.svg?branch=master)](https://coveralls.io/github/fogfish/oauth2?branch=master) 6 | 7 | ## Key Features and Functionality 8 | 9 | The appliance architecture and design reflect the principles of incremental scalability, decentralization and fault tolerance. The appliance targets no configuration experience for cloud operation and deployment. 10 | 11 | **OAuth 2.0 grants flow**: It supports out of the box grants defined by RFC 6749: Authorization code grant, Implicit grant, Client credentials grant, Resource owner password credentials grant and Refresh Token grant. 12 | 13 | **Required client identity**: [RFC 6749](https://tools.ietf.org/html/rfc6749#section-3.2.1) discusses about client authentication. This implementation requires HTTP basic digest schema to identity confidential clients and demands `client_id` parameter to identify public clients when sending requests to service endpoints. 14 | 15 | **Account settings dashboard**: provides reference implementation of account setting dashboard using react.js 16 | 17 | **Account federation** supports an integration with 3rd party services such GitHub 18 | 19 | 20 | ## Getting Started 21 | 22 | The appliance supplies pre-built releases for Linux/x86_64 and Docker platforms. Instructions for using these binaries are on the [GitHub releases page](https://github.com/fogfish/oauth2/releases). 23 | 24 | Build the latest version of authorization server from the `master` branch. The build process requires [Erlang/OTP](http://www.erlang.org/downloads) version 19.0 or later. All development, including new features and bug fixes, take place on the `master` branch using forking and pull requests as described in contribution guidelines. 25 | 26 | Build toolchain 27 | 28 | ```bash 29 | npm install -g typescript 30 | npm install -g aws-cdk 31 | npm install -g ts-node 32 | ``` 33 | 34 | ### Running authorization server 35 | 36 | The easiest way to run the appliance is with the Docker container. The option is viable only if you have configured Docker development environment on your platform. Use the latest [release version](https://github.com/fogfish/oauth2/releases): 37 | 38 | User either 39 | 40 | ``` 41 | make dist-up 42 | ``` 43 | 44 | or 45 | 46 | ``` 47 | docker-compose up 48 | ``` 49 | 50 | This starts a local instances of required backing services (e.g. DynamoDB, Redis, etc), authorization service itself and exposed OAuth 2.0 services using REST API on port 8080. By default, it is bound to `localhost` on Mac OS and Linux. If you're using a different platform, please check your Docker configuration. 51 | 52 | Open `http://localhost:8080/oauth2/developer` in your web browser to manage accounts and integrate OAuth 2.0 clients. 53 | 54 | 55 | ## Next Steps 56 | 57 | * study [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749) and its authorization flows. 58 | * [installation and configuration guidelines](docs/install.md) to the cloud for production operation. 59 | 60 | 61 | ## Contribution 62 | 63 | OAuth 2.0 is Apache 2.0 licensed and accepts contributions via GitHub pull requests: 64 | 65 | * Fork the repository on GitHub 66 | * Read build instructions 67 | * Make a pull request 68 | 69 | The build process requires [Erlang/OTP](http://www.erlang.org/downloads) version 19.0 or later and essential build tools. 70 | 71 | **Build** and **run** authorization service in your development console. The following command boots Erlang virtual machine and opens Erlang shell. 72 | 73 | ``` 74 | git clone https://github.com/fogfish/oauth2 75 | cd oauth2 76 | make 77 | make run 78 | ``` 79 | 80 | The development of authorization server requires ensemble of **backing services** (e.g. DynamoDB, Redis, etc). 81 | 82 | ``` 83 | docker-compose -f rel/service.yaml up 84 | ``` 85 | 86 | Now you are able to start oauth2 is **debug** mode. You shall be able to use OAuth 2.0 REST API once you launch application: 87 | 88 | ```erlang 89 | oauth2:start(). 90 | ``` 91 | 92 | **Package** the application into docker container. 93 | 94 | ```bash 95 | make clean && make && make release && make docker 96 | ``` 97 | The archive `oauth2-{vsn}.{arch}.{plat}.bundle` contains both a Erlang VM, all required dependencies and the application. 98 | 99 | 100 | ### commit message 101 | 102 | The commit message helps us to write a good release note, speed-up review process. The message should address two question what changed and why. The project follows the template defined by chapter [Contributing to a Project](http://git-scm.com/book/ch5-2.html) of Git book. 103 | 104 | > 105 | > Short (50 chars or less) summary of changes 106 | > 107 | > More detailed explanatory text, if necessary. Wrap it to about 72 characters or so. In some contexts, the first line is treated as the subject of an email and the rest of the text as the body. The blank line separating the summary from the body is critical (unless you omit the body entirely); tools like rebase can get confused if you run the two together. 108 | > 109 | > Further paragraphs come after blank lines. 110 | > 111 | > Bullet points are okay, too 112 | > 113 | > Typically a hyphen or asterisk is used for the bullet, preceded by a single space, with blank lines in between, but conventions vary here 114 | > 115 | > 116 | 117 | ### bugs 118 | 119 | If you experience any issues with OAuth 2.0 appliance, please let us know via [GitHub issues](https://github.com/fogfish/oauth2/issue). We appreciate detailed and accurate reports that help us to identity and replicate the issue. 120 | 121 | * **Specify** the configuration of your environment. Include which operating system you use and the versions of runtime environments. 122 | 123 | * **Attach** logs, screenshots and exceptions, in possible. 124 | 125 | * **Reveal** the steps you took to reproduce the problem. 126 | 127 | 128 | ### security issues 129 | 130 | If you discover any security related issues, please [email](mailto:dmkolesnikov@gmail.com) instead of using the issue tracker. 131 | 132 | ## Changelog 133 | 134 | The appliance uses [semantic versions](http://semver.org) to identify stable releases. 135 | 136 | * 0.3.0 - support for https 137 | * 0.2.0 - production ready release candidate 138 | * 0.0.0 - initial release for testing purpose 139 | 140 | ## References 141 | 142 | 1. [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749) 143 | 1. [OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662) 144 | 1. [JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) 145 | 146 | 147 | 148 | ## License 149 | 150 | Copyright 2017 Dmitry Kolesnikov 151 | 152 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 153 | 154 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 155 | -------------------------------------------------------------------------------- /apps/authorize/Makefile: -------------------------------------------------------------------------------- 1 | APP = authorize 2 | EVENT ?= test/signup.json 3 | 4 | include ../../serverless.mk 5 | -------------------------------------------------------------------------------- /apps/authorize/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, []}. 2 | 3 | {deps, [ 4 | {serverless, ".*", 5 | {git, "https://github.com/fogfish/serverless", {branch, master}} 6 | } 7 | 8 | , {feta, ".*", 9 | {git, "https://github.com/fogfish/feta", {branch, master}} 10 | } 11 | 12 | , {oauth2, {path, "../../libs/oauth2"}} 13 | ]}. 14 | 15 | {profiles, [ 16 | {test, [{deps, [meck]}]} 17 | ]}. 18 | 19 | {plugins, [ 20 | rebar3_path_deps 21 | ]}. 22 | 23 | %% 24 | %% 25 | {escript_main_app , authorize}. 26 | {escript_emu_args , "%%! -smp -sbt ts +A10 +K true\n"}. 27 | {escript_incl_apps , [serverless]}. 28 | -------------------------------------------------------------------------------- /apps/authorize/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"base16">>,{pkg,<<"base16">>,<<"1.0.0">>},2}, 3 | {<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},3}, 4 | {<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},3}, 5 | {<<"datum">>,{pkg,<<"datum">>,<<"4.6.1">>},1}, 6 | {<<"ddb">>, 7 | {git,"https://github.com/fogfish/ddb", 8 | {ref,"8486e553bd96e2646a1cf27e33c345c2f22ec1d7"}}, 9 | 1}, 10 | {<<"eini">>,{pkg,<<"eini">>,<<"1.2.6">>},2}, 11 | {<<"erlcloud">>,{pkg,<<"erlcloud">>,<<"3.2.7">>},1}, 12 | {<<"feta">>, 13 | {git,"https://github.com/fogfish/feta", 14 | {ref,"c5d251b3f995b96afd5e8ec7da61516842aa658c"}}, 15 | 0}, 16 | {<<"hackney">>,{pkg,<<"hackney">>,<<"1.15.1">>},2}, 17 | {<<"hash">>, 18 | {git,"https://github.com/fogfish/hash", 19 | {ref,"a1b9101189e115b4eabbe941639f3c626614e986"}}, 20 | 2}, 21 | {<<"idna">>,{pkg,<<"idna">>,<<"6.0.0">>},3}, 22 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.10.0">>},1}, 23 | {<<"jwt">>,{pkg,<<"jwt">>,<<"0.1.9">>},2}, 24 | {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.6.2">>},2}, 25 | {<<"m_http">>,{pkg,<<"m_http">>,<<"0.2.2">>},1}, 26 | {<<"meck">>,{pkg,<<"meck">>,<<"0.8.13">>},3}, 27 | {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},3}, 28 | {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},3}, 29 | {<<"oauth2">>, 30 | {path,"../../libs/oauth2",{mtime,<<"2019-12-24T13:38:23Z">>}}, 31 | 0}, 32 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},4}, 33 | {<<"permit">>, 34 | {git,"https://github.com/fogfish/permit", 35 | {ref,"7ace1307d2dd312aa140f3710efdbbba75c8f451"}}, 36 | 1}, 37 | {<<"pipe">>,{pkg,<<"pipes">>,<<"2.0.1">>},1}, 38 | {<<"pipes">>,{pkg,<<"pipes">>,<<"2.0.1">>},2}, 39 | {<<"pns">>, 40 | {git,"https://github.com/fogfish/pns", 41 | {ref,"f9b37630d0a8797063a2a443b42f14353ad858c8"}}, 42 | 3}, 43 | {<<"pts">>, 44 | {git,"https://github.com/fogfish/pts", 45 | {ref,"f98c594f1e596f81b5e54c9791bf69568327fba2"}}, 46 | 2}, 47 | {<<"serverless">>, 48 | {git,"https://github.com/fogfish/serverless", 49 | {ref,"c804e510e9c473941b1b457ba175f537f1939f39"}}, 50 | 0}, 51 | {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.4">>},3}, 52 | {<<"uid">>,{pkg,<<"uid">>,<<"1.3.4">>},2}, 53 | {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.4.1">>},4}]}. 54 | [ 55 | {pkg_hash,[ 56 | {<<"base16">>, <<"283644E2B21BD5915ACB7178BED7851FB07C6E5749B8FAD68A53C501092176D9">>}, 57 | {<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>}, 58 | {<<"certifi">>, <<"867CE347F7C7D78563450A18A6A28A8090331E77FA02380B4A21962A65D36EE5">>}, 59 | {<<"datum">>, <<"93B131203A60CFEA9FFFF6435A50DC24239F689DFEBB76E6AECF6CE689EFE8F4">>}, 60 | {<<"eini">>, <<"DFFA48476FD89FB6E41CEEA0ADFA1BC6E7862CCD6584417442F8BB37E5D34715">>}, 61 | {<<"erlcloud">>, <<"85A947B7F53C58A884590423437670679B61AF03536D7FB583BEEB2B5054D0F2">>}, 62 | {<<"hackney">>, <<"9F8F471C844B8CE395F7B6D8398139E26DDCA9EBC171A8B91342EE15A19963F4">>}, 63 | {<<"idna">>, <<"689C46CBCDF3524C44D5F3DDE8001F364CD7608A99556D8FBD8239A5798D4C10">>}, 64 | {<<"jsx">>, <<"77760560D6AC2B8C51FD4C980E9E19B784016AA70BE354CE746472C33BEB0B1C">>}, 65 | {<<"jwt">>, <<"F5687B0168AE3AA1130E0068C38A7D256E622789BF4724578ADF99A7CE5531DC">>}, 66 | {<<"lhttpc">>, <<"044F16F0018C7AA7E945E9E9406C7F6035E0B8BC08BF77B00C78CE260E1071E3">>}, 67 | {<<"m_http">>, <<"A869974B4651658786BCE18F2019E9B8DA5E5F8AEFD9A5CCE1291B941F05A2F9">>}, 68 | {<<"meck">>, <<"FFEDB39F99B0B99703B8601C6F17C7F76313EE12DE6B646E671E3188401F7866">>}, 69 | {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, 70 | {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, 71 | {<<"parse_trans">>, <<"09765507A3C7590A784615CFD421D101AEC25098D50B89D7AA1D66646BC571C1">>}, 72 | {<<"pipe">>, <<"A2B56796C63690ED0E78BB77BB389AF250BD70AFA15A6869369DBDC11087D68F">>}, 73 | {<<"pipes">>, <<"A2B56796C63690ED0E78BB77BB389AF250BD70AFA15A6869369DBDC11087D68F">>}, 74 | {<<"ssl_verify_fun">>, <<"F0EAFFF810D2041E93F915EF59899C923F4568F4585904D010387ED74988E77B">>}, 75 | {<<"uid">>, <<"42E30E22908E8E2FAA6227E9C261F1954CB540BE3C5A139E112369AE6CC451FC">>}, 76 | {<<"unicode_util_compat">>, <<"D869E4C68901DD9531385BB0C8C40444EBF624E60B6962D95952775CAC5E90CD">>}]} 77 | ]. 78 | -------------------------------------------------------------------------------- /apps/authorize/src/authorize.app.src: -------------------------------------------------------------------------------- 1 | {application, authorize, 2 | [ 3 | {description, "oauth2 authorize api"}, 4 | {vsn, "0.0.0"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications,[ 8 | kernel 9 | , stdlib 10 | , feta 11 | , oauth2 12 | ]}, 13 | {env, []} 14 | ] 15 | }. 16 | -------------------------------------------------------------------------------- /apps/authorize/src/authorize.erl: -------------------------------------------------------------------------------- 1 | -module(authorize). 2 | 3 | -compile({parse_transform, category}). 4 | -compile({parse_transform, generic}). 5 | 6 | -export([main/1]). 7 | 8 | %% 9 | %% 10 | main(Opts) -> 11 | {ok, _} = application:ensure_all_started(?MODULE), 12 | serverless:spawn(fun api/1, Opts). 13 | 14 | %% 15 | %% 16 | -spec api(map()) -> datum:either(map()). 17 | 18 | api(#{ 19 | <<"httpMethod">> := <<"POST">> 20 | } = Json) -> 21 | case dispatch(Json) of 22 | {ok, Redirect} -> 23 | serverless_api:return({302, #{<<"Location">> => uri:s(Redirect)}, <<>>}); 24 | {error, _} = Error -> 25 | serverless_api:return(Error) 26 | end; 27 | api(Json) -> 28 | serverless:warning(Json), 29 | {error, not_supported}. 30 | 31 | %% 32 | %% 33 | dispatch(#{ 34 | <<"path">> := <<"/oauth2/signin">>, 35 | <<"headers">> := #{ 36 | <<"content-type">> := <<"application/x-www-form-urlencoded", _/binary>> 37 | } = Headers, 38 | <<"body">> := Request 39 | }) -> 40 | oauth2:signin(Headers, Request); 41 | 42 | dispatch(#{ 43 | <<"path">> := <<"/oauth2/signup">>, 44 | <<"headers">> := #{ 45 | <<"content-type">> := <<"application/x-www-form-urlencoded", _/binary>> 46 | } = Headers, 47 | <<"body">> := Request 48 | }) -> 49 | oauth2:signup(Headers, Request); 50 | 51 | dispatch(#{ 52 | <<"path">> := <<"/oauth2/password">>, 53 | <<"headers">> := #{ 54 | <<"content-type">> := <<"application/x-www-form-urlencoded", _/binary>> 55 | } = Headers, 56 | <<"body">> := Request 57 | }) -> 58 | oauth2:password(Headers, Request); 59 | 60 | dispatch(Json) -> 61 | serverless:warning(Json), 62 | {error, not_supported}. 63 | -------------------------------------------------------------------------------- /apps/authorize/test/authorize_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(authorize_SUITE). 2 | 3 | -export([all/0, init_per_suite/1, end_per_suite/1]). 4 | -compile(export_all). 5 | 6 | all() -> 7 | [Test || {Test, NAry} <- ?MODULE:module_info(exports), 8 | Test =/= module_info, 9 | Test =/= init_per_suite, 10 | Test =/= end_per_suite, 11 | NAry =:= 1 12 | ]. 13 | 14 | %%%---------------------------------------------------------------------------- 15 | %%% 16 | %%% init 17 | %%% 18 | %%%---------------------------------------------------------------------------- 19 | init_per_suite(Config) -> 20 | os:putenv("PERMIT_ISSUER", "https://example.com"), 21 | os:putenv("PERMIT_AUDIENCE", "suite"), 22 | os:putenv("PERMIT_CLAIMS", "read=true&write=true"), 23 | {ok, _} = application:ensure_all_started(oauth2), 24 | Config. 25 | 26 | 27 | end_per_suite(_Config) -> 28 | ok. 29 | 30 | %% 31 | %% 32 | signup_public_client(_) -> 33 | #{ 34 | statusCode := 302 35 | , headers := #{ 36 | <<"Location">> := Location 37 | } 38 | , body := _ 39 | } = serverless:mock(authorize, 40 | #{ 41 | <<"httpMethod">> => <<"POST">> 42 | , <<"path">> => <<"/oauth2/signup">> 43 | , <<"headers">> => #{ 44 | <<"content-type">> => <<"application/x-www-form-urlencoded">> 45 | } 46 | , <<"body">> => <<"response_type=code&client_id=account@oauth2&access=joe@org&secret=secret">> 47 | } 48 | ), 49 | IDP = base64url:encode(crypto:hash(md5, <<"joe@org">>)), 50 | {ok, #{ 51 | <<"iss">> := <<"https://example.com">> 52 | , <<"aud">> := <<"oauth2">> 53 | , <<"idp">> := IDP 54 | , <<"sub">> := {iri, IDP, <<"joe@org">>} 55 | , <<"exp">> := _ 56 | , <<"tji">> := _ 57 | }} = permit:validate( uri:q(<<"code">>, undefined, uri:new(Location)) ). 58 | 59 | %% 60 | %% 61 | signin_public_client(_) -> 62 | #{ 63 | statusCode := 302 64 | , headers := #{ 65 | <<"Location">> := Location 66 | } 67 | , body := _ 68 | } = serverless:mock(authorize, 69 | #{ 70 | <<"httpMethod">> => <<"POST">> 71 | , <<"path">> => <<"/oauth2/signin">> 72 | , <<"headers">> => #{ 73 | <<"content-type">> => <<"application/x-www-form-urlencoded">> 74 | } 75 | , <<"body">> => <<"response_type=code&client_id=account@oauth2&access=joe@org&secret=secret">> 76 | } 77 | ), 78 | IDP = base64url:encode(crypto:hash(md5, <<"joe@org">>)), 79 | {ok, #{ 80 | <<"iss">> := <<"https://example.com">> 81 | , <<"aud">> := <<"oauth2">> 82 | , <<"idp">> := IDP 83 | , <<"sub">> := {iri, IDP, <<"joe@org">>} 84 | , <<"exp">> := _ 85 | , <<"tji">> := _ 86 | }} = permit:validate( uri:q(<<"code">>, undefined, uri:new(Location)) ). 87 | -------------------------------------------------------------------------------- /apps/authorize/test/signup.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpMethod": "POST", 3 | "path": "/oauth2/signup", 4 | "headers": { 5 | "content-type": "application/x-www-form-urlencoded" 6 | }, 7 | "body": "response_type=code&client_id=account@oauth2&access=access@org&secret=secret" 8 | } -------------------------------------------------------------------------------- /apps/authorize/test/tests.config: -------------------------------------------------------------------------------- 1 | {suites, ".", all}. 2 | -------------------------------------------------------------------------------- /apps/client/Makefile: -------------------------------------------------------------------------------- 1 | APP = client 2 | EVENT ?= 3 | 4 | include ../../serverless.mk 5 | -------------------------------------------------------------------------------- /apps/client/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, []}. 2 | 3 | {deps, [ 4 | {serverless, ".*", 5 | {git, "https://github.com/fogfish/serverless", {branch, master}} 6 | } 7 | 8 | , {feta, ".*", 9 | {git, "https://github.com/fogfish/feta", {branch, master}} 10 | } 11 | 12 | , {oauth2, {path, "../../libs/oauth2"}} 13 | ]}. 14 | 15 | {profiles, [ 16 | {test, [{deps, [meck]}]} 17 | ]}. 18 | 19 | {plugins, [ 20 | rebar3_path_deps 21 | ]}. 22 | 23 | %% 24 | %% 25 | {escript_main_app , client}. 26 | {escript_emu_args , "%%! -smp -sbt ts +A10 +K true\n"}. 27 | {escript_incl_apps , [serverless]}. 28 | -------------------------------------------------------------------------------- /apps/client/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"base16">>,{pkg,<<"base16">>,<<"1.0.0">>},2}, 3 | {<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},3}, 4 | {<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},3}, 5 | {<<"datum">>,{pkg,<<"datum">>,<<"4.6.1">>},1}, 6 | {<<"ddb">>, 7 | {git,"https://github.com/fogfish/ddb", 8 | {ref,"8486e553bd96e2646a1cf27e33c345c2f22ec1d7"}}, 9 | 1}, 10 | {<<"eini">>,{pkg,<<"eini">>,<<"1.2.6">>},2}, 11 | {<<"erlcloud">>,{pkg,<<"erlcloud">>,<<"3.2.7">>},1}, 12 | {<<"feta">>, 13 | {git,"https://github.com/fogfish/feta", 14 | {ref,"c5d251b3f995b96afd5e8ec7da61516842aa658c"}}, 15 | 0}, 16 | {<<"hackney">>,{pkg,<<"hackney">>,<<"1.15.1">>},2}, 17 | {<<"hash">>, 18 | {git,"https://github.com/fogfish/hash", 19 | {ref,"a1b9101189e115b4eabbe941639f3c626614e986"}}, 20 | 2}, 21 | {<<"idna">>,{pkg,<<"idna">>,<<"6.0.0">>},3}, 22 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.10.0">>},1}, 23 | {<<"jwt">>,{pkg,<<"jwt">>,<<"0.1.9">>},2}, 24 | {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.6.2">>},2}, 25 | {<<"m_http">>,{pkg,<<"m_http">>,<<"0.2.2">>},1}, 26 | {<<"meck">>,{pkg,<<"meck">>,<<"0.8.13">>},3}, 27 | {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},3}, 28 | {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},3}, 29 | {<<"oauth2">>, 30 | {path,"../../libs/oauth2",{mtime,<<"2019-12-24T13:38:23Z">>}}, 31 | 0}, 32 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},4}, 33 | {<<"permit">>, 34 | {git,"https://github.com/fogfish/permit", 35 | {ref,"7ace1307d2dd312aa140f3710efdbbba75c8f451"}}, 36 | 1}, 37 | {<<"pipe">>,{pkg,<<"pipes">>,<<"2.0.1">>},1}, 38 | {<<"pipes">>,{pkg,<<"pipes">>,<<"2.0.1">>},2}, 39 | {<<"pns">>, 40 | {git,"https://github.com/fogfish/pns", 41 | {ref,"f9b37630d0a8797063a2a443b42f14353ad858c8"}}, 42 | 3}, 43 | {<<"pts">>, 44 | {git,"https://github.com/fogfish/pts", 45 | {ref,"f98c594f1e596f81b5e54c9791bf69568327fba2"}}, 46 | 2}, 47 | {<<"serverless">>, 48 | {git,"https://github.com/fogfish/serverless", 49 | {ref,"c804e510e9c473941b1b457ba175f537f1939f39"}}, 50 | 0}, 51 | {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.4">>},3}, 52 | {<<"uid">>,{pkg,<<"uid">>,<<"1.3.4">>},2}, 53 | {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.4.1">>},4}]}. 54 | [ 55 | {pkg_hash,[ 56 | {<<"base16">>, <<"283644E2B21BD5915ACB7178BED7851FB07C6E5749B8FAD68A53C501092176D9">>}, 57 | {<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>}, 58 | {<<"certifi">>, <<"867CE347F7C7D78563450A18A6A28A8090331E77FA02380B4A21962A65D36EE5">>}, 59 | {<<"datum">>, <<"93B131203A60CFEA9FFFF6435A50DC24239F689DFEBB76E6AECF6CE689EFE8F4">>}, 60 | {<<"eini">>, <<"DFFA48476FD89FB6E41CEEA0ADFA1BC6E7862CCD6584417442F8BB37E5D34715">>}, 61 | {<<"erlcloud">>, <<"85A947B7F53C58A884590423437670679B61AF03536D7FB583BEEB2B5054D0F2">>}, 62 | {<<"hackney">>, <<"9F8F471C844B8CE395F7B6D8398139E26DDCA9EBC171A8B91342EE15A19963F4">>}, 63 | {<<"idna">>, <<"689C46CBCDF3524C44D5F3DDE8001F364CD7608A99556D8FBD8239A5798D4C10">>}, 64 | {<<"jsx">>, <<"77760560D6AC2B8C51FD4C980E9E19B784016AA70BE354CE746472C33BEB0B1C">>}, 65 | {<<"jwt">>, <<"F5687B0168AE3AA1130E0068C38A7D256E622789BF4724578ADF99A7CE5531DC">>}, 66 | {<<"lhttpc">>, <<"044F16F0018C7AA7E945E9E9406C7F6035E0B8BC08BF77B00C78CE260E1071E3">>}, 67 | {<<"m_http">>, <<"A869974B4651658786BCE18F2019E9B8DA5E5F8AEFD9A5CCE1291B941F05A2F9">>}, 68 | {<<"meck">>, <<"FFEDB39F99B0B99703B8601C6F17C7F76313EE12DE6B646E671E3188401F7866">>}, 69 | {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, 70 | {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, 71 | {<<"parse_trans">>, <<"09765507A3C7590A784615CFD421D101AEC25098D50B89D7AA1D66646BC571C1">>}, 72 | {<<"pipe">>, <<"A2B56796C63690ED0E78BB77BB389AF250BD70AFA15A6869369DBDC11087D68F">>}, 73 | {<<"pipes">>, <<"A2B56796C63690ED0E78BB77BB389AF250BD70AFA15A6869369DBDC11087D68F">>}, 74 | {<<"ssl_verify_fun">>, <<"F0EAFFF810D2041E93F915EF59899C923F4568F4585904D010387ED74988E77B">>}, 75 | {<<"uid">>, <<"42E30E22908E8E2FAA6227E9C261F1954CB540BE3C5A139E112369AE6CC451FC">>}, 76 | {<<"unicode_util_compat">>, <<"D869E4C68901DD9531385BB0C8C40444EBF624E60B6962D95952775CAC5E90CD">>}]} 77 | ]. 78 | -------------------------------------------------------------------------------- /apps/client/src/client.app.src: -------------------------------------------------------------------------------- 1 | {application, client, 2 | [ 3 | {description, "oauth2 client api"}, 4 | {vsn, "0.0.0"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications,[ 8 | kernel 9 | , stdlib 10 | , feta 11 | , oauth2 12 | ]}, 13 | {env, []} 14 | ] 15 | }. 16 | -------------------------------------------------------------------------------- /apps/client/src/client.erl: -------------------------------------------------------------------------------- 1 | -module(client). 2 | 3 | -compile({parse_transform, category}). 4 | -compile({parse_transform, generic}). 5 | 6 | -export([main/1]). 7 | 8 | %% 9 | %% 10 | main(Opts) -> 11 | {ok, _} = application:ensure_all_started(?MODULE), 12 | serverless:spawn(fun api/1, Opts). 13 | 14 | %% 15 | %% 16 | -spec api(map()) -> datum:either(map()). 17 | 18 | api(#{ 19 | <<"httpMethod">> := <<"GET">>, 20 | <<"path">> := <<"/oauth2/client">>, 21 | <<"headers">> := #{ 22 | <<"Authorization">> := <<"Bearer ", Token/binary>> 23 | } 24 | }) -> 25 | serverless_api:return( 26 | oauth2_client:lookup(Token) 27 | ); 28 | 29 | api(#{ 30 | <<"httpMethod">> := <<"POST">>, 31 | <<"path">> := <<"/oauth2/client">>, 32 | <<"headers">> := #{ 33 | <<"Authorization">> := <<"Bearer ", Token/binary>>, 34 | <<"content-type">> := <<"application/json", _/binary>> 35 | }, 36 | <<"body">> := Json 37 | }) -> 38 | serverless_api:return( 39 | oauth2_client:create(Token, jsx:decode(Json, [return_maps])) 40 | ); 41 | 42 | api(#{ 43 | <<"httpMethod">> := <<"DELETE">>, 44 | <<"path">> := <<"/oauth2/client/", _/binary>>, 45 | <<"headers">> := #{ 46 | <<"Authorization">> := <<"Bearer ", Token/binary>> 47 | }, 48 | <<"pathParameters">> := #{<<"id">> := Id} 49 | }) -> 50 | serverless_api:return( 51 | oauth2_client:remove(Token, Id) 52 | ); 53 | 54 | api(Json) -> 55 | serverless:warning(Json), 56 | {error, not_supported}. 57 | -------------------------------------------------------------------------------- /apps/client/test/client_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(client_SUITE). 2 | 3 | -export([all/0, init_per_suite/1, end_per_suite/1]). 4 | -compile(export_all). 5 | -compile({parse_transform, category}). 6 | 7 | all() -> 8 | [Test || {Test, NAry} <- ?MODULE:module_info(exports), 9 | Test =/= module_info, 10 | Test =/= init_per_suite, 11 | Test =/= end_per_suite, 12 | NAry =:= 1 13 | ]. 14 | 15 | %%%---------------------------------------------------------------------------- 16 | %%% 17 | %%% init 18 | %%% 19 | %%%---------------------------------------------------------------------------- 20 | init_per_suite(Config) -> 21 | os:putenv("PERMIT_ISSUER", "https://example.com"), 22 | os:putenv("PERMIT_AUDIENCE", "suite"), 23 | os:putenv("PERMIT_CLAIMS", "read=true&write=true"), 24 | {ok, _} = application:ensure_all_started(oauth2), 25 | permit:create({iri, <<"org">>, <<"joe">>}, <<"secret">>, #{}), 26 | Config. 27 | 28 | end_per_suite(_Config) -> 29 | ok. 30 | -------------------------------------------------------------------------------- /apps/client/test/tests.config: -------------------------------------------------------------------------------- 1 | {suites, ".", all}. 2 | -------------------------------------------------------------------------------- /apps/github/src/github.app.src: -------------------------------------------------------------------------------- 1 | {application, github, 2 | [ 3 | {description, "oauth2 authorization - github integration"}, 4 | {vsn, "0.1.0"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications,[ 8 | kernel 9 | ,stdlib 10 | ]}, 11 | {env, []} 12 | ] 13 | }. -------------------------------------------------------------------------------- /apps/github/src/github.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% @doc 3 | %% GitHub IAM 4 | -module(github). 5 | -compile({parse_transform, category}). 6 | 7 | -export([ 8 | auth_url/0, 9 | account/1 10 | ]). 11 | 12 | %% 13 | %% build auth url for service provider 14 | -spec auth_url() -> uri:uri(). 15 | 16 | auth_url() -> 17 | [option || 18 | Root <- opts:val(oauth_url, undefined, github), 19 | Scope <- opts:val(scopes, undefined, github), 20 | Client <- opts:val(access_key, github), 21 | uri:new(Root), 22 | uri:q([{client_id, Client}, {scope, Scope}], _), 23 | uri:s(_) 24 | ]. 25 | 26 | %% 27 | %% 28 | -spec account(_) -> datum:either(_). 29 | 30 | account(Url) -> 31 | [either || 32 | Client <- authenticate_oauth_client(Url), 33 | 34 | 35 | %% exchange code with github 36 | cats:optionT(badarg, uri:q(<<"code">>, undefined, Url)), 37 | authenticate_github_user(_), 38 | create_account(Client, _) 39 | ]. 40 | 41 | %% 42 | authenticate_oauth_client(Url) -> 43 | [either || 44 | %% OAuth2 implements federated accounts for GitHub users 45 | %% The feature is only available for public application 46 | %% OAuth2 state flag is used to communicate clientId across the session 47 | cats:optionT(badarg, uri:q(<<"state">>, undefined, Url)), 48 | oauth2_client:lookup(_), 49 | oauth2_client:is_public(_) 50 | ]. 51 | 52 | %% 53 | authenticate_github_user(Code) -> 54 | case (request_github_api(Code))(#{}) of 55 | [{Access, undefined} | State] -> 56 | lager:warning("[github] unauthorized attempt from ~s", [Access]), 57 | [knet:close(Sock) || Sock <- maps:values(maps:without([req, ret, so], State))], 58 | {error, unauthorized}; 59 | 60 | [{Access, Source} | State] -> 61 | lager:notice("[github] access to ~s is derived from ~s", [Access, Source]), 62 | [knet:close(Sock) || Sock <- maps:values(maps:without([req, ret, so], State))], 63 | {ok, #{access => Access, master => scalar:s(opts:val(org, github))}} 64 | end. 65 | 66 | request_github_api(Code) -> 67 | [m_state || 68 | Token <- github_access_token(Code), 69 | User <- github_user_profile(Token), 70 | Access <- allows_access_contributor(Token), 71 | cats:unit({User, Access}) 72 | ]. 73 | 74 | allows_access_contributor(Token) -> 75 | fun(State0) -> 76 | case (github_user_orgs(Token))(State0) of 77 | [undefined | State1] -> 78 | (github_user_contribution(Token))(State1); 79 | 80 | [_ | _] = State1 -> 81 | State1 82 | end 83 | end. 84 | 85 | %% 86 | %% 87 | github_access_token(Code) -> 88 | [m_http || 89 | cats:new( url(token_url) ), 90 | cats:so(#{active => true}), 91 | cats:method('POST'), 92 | cats:header("Connection", "keep-alive"), 93 | cats:header("Content-Type", "application/x-www-form-urlencoded"), 94 | cats:header("Transfer-Encoding", "chunked"), 95 | cats:payload( access_token_request(Code) ), 96 | cats:request(), 97 | cats:require(content, lens:at(<<"access_token">>)) 98 | ]. 99 | 100 | %% 101 | %% 102 | github_user_profile(Token) -> 103 | [m_http || 104 | cats:new( uri:segments([user], url(ghapi_url)) ), 105 | cats:so(#{active => true}), 106 | cats:method('GET'), 107 | cats:header("Connection", "keep-alive"), 108 | cats:header("Accept", "application/json"), 109 | cats:header("User-Agent", "knet"), 110 | cats:header("Authorization", <<"Bearer ", Token/binary>>), 111 | cats:request(), 112 | cats:require(content, lens:at(<<"login">>)) 113 | ]. 114 | 115 | %% 116 | %% 117 | github_user_orgs(Token) -> 118 | [m_http || 119 | cats:new( uri:segments([user, orgs], url(ghapi_url)) ), 120 | cats:so(#{active => true}), 121 | cats:method('GET'), 122 | cats:header("Connection", "keep-alive"), 123 | cats:header("Accept", "application/json"), 124 | cats:header("User-Agent", "knet"), 125 | cats:header("Authorization", <<"Bearer ", Token/binary>>), 126 | cats:request(), 127 | cats:require(content, 128 | lens:c( 129 | lens:takewith(fun allowed_org/1, #{}), 130 | lens:at(<<"login">>) 131 | ) 132 | ) 133 | ]. 134 | 135 | %% 136 | %% 137 | github_user_contribution(Token) -> 138 | [m_http || 139 | cats:new( uri:q([{affiliation, collaborator}], uri:segments([user, repos], url(ghapi_url))) ), 140 | cats:so(#{active => true}), 141 | cats:method('GET'), 142 | cats:header("Connection", "keep-alive"), 143 | cats:header("Accept", "application/json"), 144 | cats:header("User-Agent", "knet"), 145 | cats:header("Authorization", <<"Bearer ", Token/binary>>), 146 | cats:request(), 147 | cats:require(content, 148 | lens:c( 149 | lens:takewith(fun allowed_contrib/1, #{}), 150 | lens:at(<<"name">>) 151 | ) 152 | ) 153 | ]. 154 | 155 | allowed_org(#{<<"login">> := Org}) -> 156 | Org =:= scalar:s(opts:val(org, github)). 157 | 158 | allowed_contrib(#{<<"name">> := Name}) -> 159 | [identity || 160 | scalar:s(opts:val(contrib, github)), 161 | binary:split(_, <<$,>>, [global]), 162 | lists:member(Name, _) 163 | ]. 164 | 165 | access_token_request(Code) -> 166 | #{ 167 | client_id => opts:val(access_key, github), 168 | client_secret => opts:val(secret_key, github), 169 | code => Code 170 | }. 171 | 172 | %% 173 | %% 174 | create_account(Client, #{access := Access, master := Master}) -> 175 | Secret = crypto:strong_rand_bytes(30), 176 | case permit:lookup(Access) of 177 | {error, not_found} -> 178 | %% token here 179 | permit:create(Access, Secret, #{ 180 | <<"type">> => <<"oauth2:account">>, 181 | <<"uid">> => true, 182 | <<"master">> => Master 183 | }); 184 | 185 | {ok, _} -> 186 | permit:update(Access, Secret, #{ 187 | <<"type">> => <<"oauth2:account">>, 188 | <<"uid">> => true, 189 | <<"master">> => Master 190 | }) 191 | end, 192 | case 193 | oauth2_token:exchange_code(Access, Secret) 194 | of 195 | {ok, Code} -> 196 | redirect_uri([{code, Code}], Client); 197 | {error, _} = Error -> 198 | redirect_uri([Error], Client) 199 | end. 200 | 201 | 202 | %% 203 | %% 204 | url(Key) -> 205 | uri:new(opts:val(Key, github)). 206 | 207 | %% 208 | redirect_uri(Status, Client) -> 209 | oauth2_client:redirect_uri(Client, Status). 210 | 211 | -------------------------------------------------------------------------------- /apps/signin/src/signin.app.src: -------------------------------------------------------------------------------- 1 | {application, signin, 2 | [ 3 | {description, "oauth2 sign-in client"}, 4 | {vsn, "0.1.0"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications,[ 8 | kernel 9 | ,stdlib 10 | ]}, 11 | {env, []} 12 | ] 13 | }. -------------------------------------------------------------------------------- /apps/signin/src/signin.erl: -------------------------------------------------------------------------------- 1 | -module(signin). 2 | -compile({parse_transform, category}). 3 | 4 | -export([ 5 | main/1 6 | ]). 7 | 8 | %% 9 | %% 10 | opts() -> 11 | [ 12 | {help, $h, "help", undefined, "Print usage"} 13 | , {profile, $f, "profile", string, "Configuration profile"} 14 | , {oauth2, $a, "oauth2", string, "Authorization server"} 15 | , {client, $c, "client", string, "Identity of client application"} 16 | , {access, $u, "username", string, "Username"} 17 | , {secret, $p, "password", string, "Password"} 18 | ]. 19 | 20 | %% 21 | %% 22 | main(Args) -> 23 | {ok, {Opts, _Files}} = getopt:parse(opts(), Args), 24 | Profile = profile(Opts), 25 | case 26 | lists:member(help, Profile) orelse 27 | lens:get(lens:pair(oauth2, undefined), Profile) =:= undefined orelse 28 | lens:get(lens:pair(access, undefined), Profile) =:= undefined orelse 29 | lens:get(lens:pair(secret, undefined), Profile) =:= undefined 30 | 31 | of 32 | true -> 33 | getopt:usage(opts(), escript:script_name(), ""), 34 | halt(0); 35 | _ -> 36 | {ok, _} = application:ensure_all_started(ssl), 37 | {ok, _} = application:ensure_all_started(knet), 38 | main( 39 | lens:get(lens:pair(oauth2), Profile), 40 | lens:get(lens:pair(client, <<"oauth2-account">>), Profile), 41 | lens:get(lens:pair(access), Profile), 42 | lens:get(lens:pair(secret), Profile) 43 | ) 44 | end. 45 | 46 | profile(Opts) -> 47 | case lens:get(lens:pair(profile, undefined), Opts) of 48 | undefined -> 49 | Opts; 50 | File -> 51 | {ok, Profile} = file:consult(File), 52 | Opts ++ Profile 53 | end. 54 | 55 | %% 56 | %% 57 | main(OAuth2, Client, Access, Secret) -> 58 | case 59 | m_http:once( 60 | [m_state || 61 | signin(OAuth2, Client, Access, Secret), 62 | exchange(OAuth2, Client, _) 63 | ] 64 | ) 65 | of 66 | {ok, Token} -> 67 | file:write(standard_io, Token), 68 | halt(0); 69 | {error, Reason} -> 70 | file:write(standard_error, io_lib:format("Unable to fetch token ~p~n", [Reason])), 71 | halt(128) 72 | end. 73 | 74 | signin(OAuth2, Client, Access, Secret) -> 75 | [m_http || 76 | _ > "POST " ++ OAuth2 ++ "/oauth2/signin", 77 | _ > "Accept: */*", 78 | _ > "Connection: close", 79 | _ > "Content-Type: application/x-www-form-urlencoded", 80 | _ > #{ 81 | access => Access, 82 | secret => Secret, 83 | response_type => <<"code">>, 84 | client_id => Client, 85 | state => <<"none">> 86 | }, 87 | 88 | _ < 302, 89 | Url < "Location: _", 90 | _ < lens:c(lens:const(Url), lens_uri(fun uri:q/1), lens:pair(<<"code">>)) 91 | ]. 92 | 93 | lens_uri(Accessor) -> 94 | fun(Fun, Uri) -> 95 | lens:fmap(fun(_) -> Uri end, Fun( Accessor(uri:new(Uri)) )) 96 | end. 97 | 98 | exchange(OAuth2, Client, Code) -> 99 | [m_http || 100 | _ > "POST " ++ OAuth2 ++ "/oauth2/token", 101 | _ > "Accept: */*", 102 | _ > "Connection: close", 103 | _ > "Content-Type: application/x-www-form-urlencoded", 104 | _ > #{ 105 | grant_type => <<"authorization_code">>, 106 | client_id => Client, 107 | code => Code 108 | }, 109 | 110 | _ < 200, 111 | _ < "Content-Type: application/json", 112 | _ < lens:at(<<"access_token">>) 113 | ]. 114 | -------------------------------------------------------------------------------- /apps/token/Makefile: -------------------------------------------------------------------------------- 1 | APP = token 2 | EVENT ?= 3 | 4 | include ../../serverless.mk 5 | -------------------------------------------------------------------------------- /apps/token/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, []}. 2 | 3 | {deps, [ 4 | {serverless, ".*", 5 | {git, "https://github.com/fogfish/serverless", {branch, master}} 6 | } 7 | 8 | , {feta, ".*", 9 | {git, "https://github.com/fogfish/feta", {branch, master}} 10 | } 11 | 12 | , {oauth2, {path, "../../libs/oauth2"}} 13 | ]}. 14 | 15 | {profiles, [ 16 | {test, [{deps, [meck]}]} 17 | ]}. 18 | 19 | {plugins, [ 20 | rebar3_path_deps 21 | ]}. 22 | 23 | %% 24 | %% 25 | {escript_main_app , token}. 26 | {escript_emu_args , "%%! -smp -sbt ts +A10 +K true\n"}. 27 | {escript_incl_apps , [serverless]}. 28 | -------------------------------------------------------------------------------- /apps/token/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"base16">>,{pkg,<<"base16">>,<<"1.0.0">>},2}, 3 | {<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},3}, 4 | {<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},3}, 5 | {<<"datum">>,{pkg,<<"datum">>,<<"4.6.1">>},1}, 6 | {<<"ddb">>, 7 | {git,"https://github.com/fogfish/ddb", 8 | {ref,"8486e553bd96e2646a1cf27e33c345c2f22ec1d7"}}, 9 | 1}, 10 | {<<"eini">>,{pkg,<<"eini">>,<<"1.2.6">>},2}, 11 | {<<"erlcloud">>,{pkg,<<"erlcloud">>,<<"3.2.7">>},1}, 12 | {<<"feta">>, 13 | {git,"https://github.com/fogfish/feta", 14 | {ref,"c5d251b3f995b96afd5e8ec7da61516842aa658c"}}, 15 | 0}, 16 | {<<"hackney">>,{pkg,<<"hackney">>,<<"1.15.1">>},2}, 17 | {<<"hash">>, 18 | {git,"https://github.com/fogfish/hash", 19 | {ref,"a1b9101189e115b4eabbe941639f3c626614e986"}}, 20 | 2}, 21 | {<<"idna">>,{pkg,<<"idna">>,<<"6.0.0">>},3}, 22 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.10.0">>},1}, 23 | {<<"jwt">>,{pkg,<<"jwt">>,<<"0.1.9">>},2}, 24 | {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.6.2">>},2}, 25 | {<<"m_http">>,{pkg,<<"m_http">>,<<"0.2.2">>},1}, 26 | {<<"meck">>,{pkg,<<"meck">>,<<"0.8.13">>},3}, 27 | {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},3}, 28 | {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},3}, 29 | {<<"oauth2">>, 30 | {path,"../../libs/oauth2",{mtime,<<"2019-12-24T13:38:23Z">>}}, 31 | 0}, 32 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},4}, 33 | {<<"permit">>, 34 | {git,"https://github.com/fogfish/permit", 35 | {ref,"7ace1307d2dd312aa140f3710efdbbba75c8f451"}}, 36 | 1}, 37 | {<<"pipe">>,{pkg,<<"pipes">>,<<"2.0.1">>},1}, 38 | {<<"pipes">>,{pkg,<<"pipes">>,<<"2.0.1">>},2}, 39 | {<<"pns">>, 40 | {git,"https://github.com/fogfish/pns", 41 | {ref,"f9b37630d0a8797063a2a443b42f14353ad858c8"}}, 42 | 3}, 43 | {<<"pts">>, 44 | {git,"https://github.com/fogfish/pts", 45 | {ref,"f98c594f1e596f81b5e54c9791bf69568327fba2"}}, 46 | 2}, 47 | {<<"serverless">>, 48 | {git,"https://github.com/fogfish/serverless", 49 | {ref,"c804e510e9c473941b1b457ba175f537f1939f39"}}, 50 | 0}, 51 | {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.4">>},3}, 52 | {<<"uid">>,{pkg,<<"uid">>,<<"1.3.4">>},2}, 53 | {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.4.1">>},4}]}. 54 | [ 55 | {pkg_hash,[ 56 | {<<"base16">>, <<"283644E2B21BD5915ACB7178BED7851FB07C6E5749B8FAD68A53C501092176D9">>}, 57 | {<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>}, 58 | {<<"certifi">>, <<"867CE347F7C7D78563450A18A6A28A8090331E77FA02380B4A21962A65D36EE5">>}, 59 | {<<"datum">>, <<"93B131203A60CFEA9FFFF6435A50DC24239F689DFEBB76E6AECF6CE689EFE8F4">>}, 60 | {<<"eini">>, <<"DFFA48476FD89FB6E41CEEA0ADFA1BC6E7862CCD6584417442F8BB37E5D34715">>}, 61 | {<<"erlcloud">>, <<"85A947B7F53C58A884590423437670679B61AF03536D7FB583BEEB2B5054D0F2">>}, 62 | {<<"hackney">>, <<"9F8F471C844B8CE395F7B6D8398139E26DDCA9EBC171A8B91342EE15A19963F4">>}, 63 | {<<"idna">>, <<"689C46CBCDF3524C44D5F3DDE8001F364CD7608A99556D8FBD8239A5798D4C10">>}, 64 | {<<"jsx">>, <<"77760560D6AC2B8C51FD4C980E9E19B784016AA70BE354CE746472C33BEB0B1C">>}, 65 | {<<"jwt">>, <<"F5687B0168AE3AA1130E0068C38A7D256E622789BF4724578ADF99A7CE5531DC">>}, 66 | {<<"lhttpc">>, <<"044F16F0018C7AA7E945E9E9406C7F6035E0B8BC08BF77B00C78CE260E1071E3">>}, 67 | {<<"m_http">>, <<"A869974B4651658786BCE18F2019E9B8DA5E5F8AEFD9A5CCE1291B941F05A2F9">>}, 68 | {<<"meck">>, <<"FFEDB39F99B0B99703B8601C6F17C7F76313EE12DE6B646E671E3188401F7866">>}, 69 | {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, 70 | {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, 71 | {<<"parse_trans">>, <<"09765507A3C7590A784615CFD421D101AEC25098D50B89D7AA1D66646BC571C1">>}, 72 | {<<"pipe">>, <<"A2B56796C63690ED0E78BB77BB389AF250BD70AFA15A6869369DBDC11087D68F">>}, 73 | {<<"pipes">>, <<"A2B56796C63690ED0E78BB77BB389AF250BD70AFA15A6869369DBDC11087D68F">>}, 74 | {<<"ssl_verify_fun">>, <<"F0EAFFF810D2041E93F915EF59899C923F4568F4585904D010387ED74988E77B">>}, 75 | {<<"uid">>, <<"42E30E22908E8E2FAA6227E9C261F1954CB540BE3C5A139E112369AE6CC451FC">>}, 76 | {<<"unicode_util_compat">>, <<"D869E4C68901DD9531385BB0C8C40444EBF624E60B6962D95952775CAC5E90CD">>}]} 77 | ]. 78 | -------------------------------------------------------------------------------- /apps/token/src/token.app.src: -------------------------------------------------------------------------------- 1 | {application, token, 2 | [ 3 | {description, "oauth2 token api"}, 4 | {vsn, "0.0.0"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications,[ 8 | kernel 9 | , stdlib 10 | , feta 11 | , oauth2 12 | ]}, 13 | {env, []} 14 | ] 15 | }. 16 | -------------------------------------------------------------------------------- /apps/token/src/token.erl: -------------------------------------------------------------------------------- 1 | -module(token). 2 | 3 | -compile({parse_transform, category}). 4 | -compile({parse_transform, generic}). 5 | 6 | -export([main/1]). 7 | 8 | %% 9 | %% 10 | main(Opts) -> 11 | {ok, _} = application:ensure_all_started(?MODULE), 12 | serverless:spawn(fun api/1, Opts). 13 | 14 | %% 15 | %% 16 | -spec api(map()) -> datum:either(map()). 17 | 18 | api(#{ 19 | <<"httpMethod">> := <<"POST">>, 20 | <<"path">> := <<"/oauth2/token">>, 21 | <<"headers">> := #{ 22 | <<"content-type">> := <<"application/x-www-form-urlencoded", _/binary>> 23 | } = Headers, 24 | <<"body">> := Request 25 | }) -> 26 | serverless_api:return(oauth2:token(Headers, Request)); 27 | 28 | %% 29 | %% https://tools.ietf.org/html/rfc7662 30 | %% Section 2.1. Introspection Request 31 | %% To prevent token scanning attacks, the endpoint MUST also require 32 | %% some form of authorization to access this endpoint. 33 | %% (The end-point is only available for confidential clients) 34 | %% 35 | api(#{ 36 | <<"httpMethod">> := <<"POST">>, 37 | <<"path">> := <<"/oauth2/introspect">>, 38 | <<"headers">> := #{ 39 | <<"content-type">> := <<"application/x-www-form-urlencoded", _/binary>>, 40 | <<"Authorization">> := Digest 41 | }, 42 | <<"body">> := Request 43 | }) -> 44 | case 45 | [either || 46 | oauth2_client:confidential(Digest), 47 | oauth2_codec:decode(Request), 48 | cats:optionT(badarg, lens:get(lens:at(<<"token">>, undefined), _)), 49 | permit:validate(_) 50 | ] 51 | of 52 | {ok, Claims} -> 53 | serverless_api:return({ok, Claims#{<<"active">> => true}}); 54 | {error, _} -> 55 | serverless_api:return({ok, #{<<"active">> => false}}) 56 | end; 57 | 58 | api(Json) -> 59 | serverless:warning(Json), 60 | {error, not_supported}. 61 | -------------------------------------------------------------------------------- /apps/token/test/tests.config: -------------------------------------------------------------------------------- 1 | {suites, ".", all}. 2 | -------------------------------------------------------------------------------- /apps/token/test/token_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(token_SUITE). 2 | 3 | -export([all/0, init_per_suite/1, end_per_suite/1]). 4 | -compile(export_all). 5 | -compile({parse_transform, category}). 6 | 7 | all() -> 8 | [Test || {Test, NAry} <- ?MODULE:module_info(exports), 9 | Test =/= module_info, 10 | Test =/= init_per_suite, 11 | Test =/= end_per_suite, 12 | NAry =:= 1 13 | ]. 14 | 15 | %%%---------------------------------------------------------------------------- 16 | %%% 17 | %%% init 18 | %%% 19 | %%%---------------------------------------------------------------------------- 20 | init_per_suite(Config) -> 21 | os:putenv("PERMIT_ISSUER", "https://example.com"), 22 | os:putenv("PERMIT_AUDIENCE", "suite"), 23 | os:putenv("PERMIT_CLAIMS", "read=true&write=true"), 24 | {ok, _} = application:ensure_all_started(oauth2), 25 | IDP = base64url:encode(crypto:hash(md5, <<"joe@org">>)), 26 | permit:create({iri, IDP, <<"joe@org">>}, <<"secret">>, #{}), 27 | Config. 28 | 29 | end_per_suite(_Config) -> 30 | ok. 31 | 32 | %% 33 | %% 34 | code_exchange(_) -> 35 | IDP = base64url:encode(crypto:hash(md5, <<"joe@org">>)), 36 | {ok, Code} = [either || 37 | permit:stateless({iri, IDP, <<"joe@org">>}, <<"secret">>, 10, #{}), 38 | oauth2_authorize:exchange_code(_, #{}) 39 | ], 40 | #{ 41 | statusCode := 200 42 | , body := Json 43 | } = serverless:mock(token, 44 | #{ 45 | <<"httpMethod">> => <<"POST">> 46 | , <<"path">> => <<"/oauth2/token">> 47 | , <<"headers">> => #{ 48 | <<"content-type">> => <<"application/x-www-form-urlencoded">> 49 | } 50 | , <<"body">> => <<"grant_type=authorization_code&client_id=account@oauth2&code=", Code/binary>> 51 | } 52 | ), 53 | #{ 54 | <<"token_type">> := <<"bearer">> 55 | , <<"access_token">> := AccessToken 56 | , <<"expires_in">> := _ 57 | , <<"aud">> := <<"suite">> 58 | , <<"idp">> := IDP 59 | , <<"iss">> := <<"https://example.com">> 60 | , <<"sub">> := <<"joe@org">> 61 | , <<"exp">> := _ 62 | , <<"tji">> := _ 63 | } = jsx:decode(Json, [return_maps]), 64 | {ok, #{ 65 | <<"iss">> := <<"https://example.com">> 66 | , <<"aud">> := <<"suite">> 67 | , <<"idp">> := IDP 68 | , <<"sub">> := {iri, IDP, <<"joe@org">>} 69 | , <<"exp">> := _ 70 | , <<"tji">> := _ 71 | , <<"rev">> := true 72 | }} = permit:validate( AccessToken ). 73 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | ## 2 | ## see https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html 3 | version: 0.2 4 | 5 | phases: 6 | install: 7 | commands: 8 | - echo "==> install" 9 | - make deps 10 | 11 | pre_build: 12 | commands: 13 | - echo "==> testing" 14 | - make test 15 | 16 | build: 17 | commands: 18 | - echo "==> build" 19 | - make dist 20 | 21 | post_build: 22 | commands: 23 | - echo "==> post build" 24 | - (cd cloud && cdk deploy oauth2-api-${BUILD_RELEASE} -c vsn=${BUILD_RELEASE} -c domain=${CONFIG_DOMAIN} -c email=${CONFIG_EMAIL} -c cert=${CONFIG_TLS_CERT}) 25 | - make -C js/signin dist-up VSN=${BUILD_RELEASE} 26 | - make -C js/account dist-up VSN=${BUILD_RELEASE} 27 | -------------------------------------------------------------------------------- /cleanspec.yml: -------------------------------------------------------------------------------- 1 | ## 2 | ## see https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html 3 | version: 0.2 4 | 5 | phases: 6 | install: 7 | commands: 8 | - echo "==> install" 9 | - npm --prefix cloud install 10 | - make -C apps/authorize deps 11 | - make -C apps/token deps 12 | - make -C apps/client deps 13 | 14 | pre_build: 15 | commands: 16 | - echo "==> pre build" 17 | build: 18 | commands: 19 | - echo "==> build" 20 | - make -C apps/authorize dist VSN=${BUILD_RELEASE} 21 | - make -C apps/token dist VSN=${BUILD_RELEASE} 22 | - make -C apps/client dist VSN=${BUILD_RELEASE} 23 | 24 | post_build: 25 | commands: 26 | - echo "==> post build" 27 | - make -C js/signin dist-rm VSN=${BUILD_RELEASE} 28 | - make -C js/account dist-rm VSN=${BUILD_RELEASE} 29 | - (cd cloud && cdk destroy -f oauth2-api-${BUILD_RELEASE} oauth2-db-${BUILD_RELEASE} -c vsn=${BUILD_RELEASE} -c domain=${CONFIG_DOMAIN} -c email=${CONFIG_EMAIL} -c cert=${CONFIG_TLS_CERT}) 30 | -------------------------------------------------------------------------------- /cloud/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "ts-node src/app", 3 | "requireApproval": "never" 4 | } -------------------------------------------------------------------------------- /cloud/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-oauth2", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@types/node": "12.12.17", 6 | "aws-cdk-pure": "^1.2.9", 7 | "aws-cdk-pure-hoc": "^1.6.2", 8 | "@aws-cdk/core": "1.18.0", 9 | "@aws-cdk/aws-iam": "1.18.0", 10 | "@aws-cdk/aws-lambda": "1.18.0", 11 | "@aws-cdk/aws-apigateway": "1.18.0", 12 | "@aws-cdk/aws-logs": "1.18.0", 13 | "@aws-cdk/aws-dynamodb": "1.18.0" 14 | }, 15 | "devDependencies": { 16 | "ts-node": "^8.5.4", 17 | "tslint": "^5.20.1", 18 | "prettier": "^1.19.1", 19 | "tslint-config-prettier": "^1.18.0", 20 | "typescript": "^3.7.3" 21 | }, 22 | "scripts": { 23 | "clean": "rm -Rf cdk.out && rm -Rf node_modules", 24 | "lint": "tslint -p tsconfig.json" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cloud/src/app.ts: -------------------------------------------------------------------------------- 1 | import * as lambda from '@aws-cdk/aws-lambda' 2 | import * as cdk from '@aws-cdk/core' 3 | import * as pure from 'aws-cdk-pure' 4 | import { gateway, staticweb } from 'aws-cdk-pure-hoc' 5 | import * as auth from './auth' 6 | import * as client from './client' 7 | import { DDB } from './storage' 8 | import * as token from './token' 9 | 10 | // ---------------------------------------------------------------------------- 11 | // 12 | // Config 13 | // 14 | // ---------------------------------------------------------------------------- 15 | const app = new cdk.App() 16 | const vsn: string = app.node.tryGetContext('vsn') || 'latest' 17 | const domain: string = app.node.tryGetContext('domain') 18 | const subdomain: string = `${vsn}.auth` 19 | const email: string = app.node.tryGetContext('email') 20 | const tlsCertificate: string = app.node.tryGetContext('cert') 21 | const host: string = `${subdomain}.${domain}` 22 | const stack = { 23 | env: { 24 | account: process.env.CDK_DEFAULT_ACCOUNT, 25 | region: process.env.CDK_DEFAULT_REGION 26 | } 27 | } 28 | 29 | // ---------------------------------------------------------------------------- 30 | // 31 | // Storage Backend 32 | // 33 | // ---------------------------------------------------------------------------- 34 | const storage = (vsn.startsWith('pr') || vsn === 'latest') 35 | ? `oauth2-db-${vsn}` 36 | : `oauth2-db-live` 37 | 38 | const dev = new cdk.Stack(app, storage, { ...stack }) 39 | const ddb = pure.join(dev, DDB(vsn)) 40 | 41 | // ---------------------------------------------------------------------------- 42 | // 43 | // API Gateway 44 | // 45 | // ---------------------------------------------------------------------------- 46 | const oauth2 = new cdk.Stack(app, `oauth2-api-${vsn}`, { ...stack }) 47 | 48 | // 49 | const api = staticweb.Gateway({ 50 | domain, 51 | sites: [ 52 | { 53 | origin: 'signin', 54 | site: 'api/oauth2/authorize', 55 | }, 56 | { 57 | origin: 'account', 58 | site: 'api/oauth2/account', 59 | } 60 | ], 61 | subdomain, 62 | tlsCertificate, 63 | }) 64 | 65 | // 66 | const Layer = (): pure.IPure => { 67 | const LAYER='erlang-serverless:4' 68 | const iaac = pure.include(lambda.LayerVersion.fromLayerVersionArn) 69 | const AuthLayer= (): string => `arn:aws:lambda:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:layer:${LAYER}` 70 | return iaac(AuthLayer) 71 | } 72 | 73 | pure.join(oauth2, 74 | pure.use({ api, runtime: Layer() }) 75 | .flatMap(x => ({ 76 | auth: auth.Function(host, email, ddb, [x.runtime]), 77 | client: client.Function(host, ddb, [x.runtime]), 78 | token: token.Function(host, ddb, [x.runtime]), 79 | })) 80 | .effect(x => { 81 | const apiOAuth2 = x.api.root.getResource('oauth2') 82 | if (apiOAuth2) { 83 | apiOAuth2.addResource('signin').addMethod('POST', x.auth) 84 | apiOAuth2.addResource('signup').addMethod('POST', x.auth) 85 | apiOAuth2.addResource('password').addMethod('POST', x.auth) 86 | 87 | apiOAuth2.addResource('token').addMethod('POST', x.token) 88 | apiOAuth2.addResource('introspect').addMethod('POST', x.token) 89 | 90 | const apiClient = gateway.CORS(apiOAuth2.addResource('client')) 91 | apiClient.addMethod('GET', x.client) 92 | apiClient.addMethod('POST', x.client) 93 | gateway.CORS(apiClient.addResource('{id}')).addMethod('DELETE', x.client) 94 | } 95 | }) 96 | ) 97 | 98 | app.synth() -------------------------------------------------------------------------------- /cloud/src/auth.ts: -------------------------------------------------------------------------------- 1 | import * as api from '@aws-cdk/aws-apigateway' 2 | import * as ddb from '@aws-cdk/aws-dynamodb' 3 | import * as iam from '@aws-cdk/aws-iam' 4 | import * as lambda from '@aws-cdk/aws-lambda' 5 | import * as logs from '@aws-cdk/aws-logs' 6 | import * as cdk from '@aws-cdk/core' 7 | import * as pure from 'aws-cdk-pure' 8 | import * as permit from './permit' 9 | 10 | // 11 | // 12 | export const Function = (host: string, email: string, db: ddb.Table, layers: lambda.ILayerVersion[]): pure.IPure => 13 | pure.wrap(api.LambdaIntegration)( 14 | Role(db).flatMap(x => Lambda(host, email, db, x, layers)) 15 | ) 16 | 17 | // 18 | const Lambda = (host: string, email: string, db: ddb.Table, role: iam.IRole, layers: lambda.ILayerVersion[]): pure.IPure => { 19 | const iaac = pure.iaac(lambda.Function) 20 | const Auth = (): lambda.FunctionProps => ({ 21 | code: new lambda.AssetCode('../apps/authorize/_build/default/bin'), 22 | environment: { 23 | 'OAUTH2_EMAIL': email, 24 | 'OAUTH2_EMAIL_SIGNATURE': 'Service', 25 | 'PERMIT_AUDIENCE': 'any', 26 | 'PERMIT_CLAIMS': '', 27 | 'PERMIT_ISSUER': `https://${host}`, 28 | 'PERMIT_KEYPAIR': 'permit_config_ddb', 29 | 'PERMIT_STORAGE': `ddb+https://dynamodb.${cdk.Aws.REGION}.amazonaws.com:443/${db.tableName}`, 30 | }, 31 | handler: 'index.main', 32 | layers, 33 | logRetention: logs.RetentionDays.FIVE_DAYS, 34 | memorySize: 256, 35 | reservedConcurrentExecutions: 5, 36 | role, 37 | runtime: lambda.Runtime.PROVIDED, 38 | timeout: cdk.Duration.seconds(10), 39 | }) 40 | return iaac(Auth) 41 | } 42 | 43 | // 44 | const Role = (db: ddb.Table): pure.IPure => { 45 | const role = pure.iaac(iam.Role) 46 | const AuthRole = (): iam.RoleProps => ({ 47 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com') 48 | }) 49 | 50 | return role(AuthRole).effect(x => { 51 | x.addToPolicy(permit.LambdaLogging()) 52 | x.addToPolicy(permit.DynamoDbReadWrite(db.tableArn)) 53 | }) 54 | } -------------------------------------------------------------------------------- /cloud/src/client.ts: -------------------------------------------------------------------------------- 1 | import * as api from '@aws-cdk/aws-apigateway' 2 | import * as ddb from '@aws-cdk/aws-dynamodb' 3 | import * as iam from '@aws-cdk/aws-iam' 4 | import * as lambda from '@aws-cdk/aws-lambda' 5 | import * as logs from '@aws-cdk/aws-logs' 6 | import * as cdk from '@aws-cdk/core' 7 | import * as pure from 'aws-cdk-pure' 8 | import * as permit from './permit' 9 | 10 | // 11 | // 12 | export const Function = (host: string, db: ddb.Table, layers: lambda.ILayerVersion[]): pure.IPure => 13 | pure.wrap(api.LambdaIntegration)( 14 | Role(db).flatMap(x => Lambda(host, db, x, layers)) 15 | ) 16 | 17 | // 18 | const Lambda = (host: string, db: ddb.Table, role: iam.IRole, layers: lambda.ILayerVersion[]): pure.IPure => { 19 | const iaac = pure.iaac(lambda.Function) 20 | const Client = (): lambda.FunctionProps => ({ 21 | code: new lambda.AssetCode('../apps/client/_build/default/bin'), 22 | environment: { 23 | 'PERMIT_AUDIENCE': 'any', 24 | 'PERMIT_CLAIMS': '', 25 | 'PERMIT_ISSUER': `https://${host}`, 26 | 'PERMIT_KEYPAIR': 'permit_config_ddb', 27 | 'PERMIT_STORAGE': `ddb+https://dynamodb.${cdk.Aws.REGION}.amazonaws.com:443/${db.tableName}` 28 | }, 29 | handler: 'index.main', 30 | layers, 31 | logRetention: logs.RetentionDays.FIVE_DAYS, 32 | memorySize: 256, 33 | reservedConcurrentExecutions: 5, 34 | role, 35 | runtime: lambda.Runtime.PROVIDED, 36 | timeout: cdk.Duration.seconds(10), 37 | }) 38 | return iaac(Client) 39 | } 40 | 41 | // 42 | const Role = (db: ddb.Table): pure.IPure => { 43 | const role = pure.iaac(iam.Role) 44 | const ClientRole = (): iam.RoleProps => ({ 45 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com') 46 | }) 47 | 48 | return role(ClientRole).effect(x => { 49 | x.addToPolicy(permit.LambdaLogging()) 50 | x.addToPolicy(permit.DynamoDbReadWrite(db.tableArn)) 51 | }) 52 | } -------------------------------------------------------------------------------- /cloud/src/permit.ts: -------------------------------------------------------------------------------- 1 | import * as iam from '@aws-cdk/aws-iam' 2 | 3 | export const LambdaLogging = (): iam.PolicyStatement => 4 | new iam.PolicyStatement({ 5 | actions: [ 6 | 'logs:CreateLogGroup', 7 | 'logs:CreateLogStream', 8 | 'logs:PutLogEvents' 9 | ], 10 | resources: ['*'], 11 | }) 12 | 13 | export const DynamoDbReadWrite = (arn: string): iam.PolicyStatement => 14 | new iam.PolicyStatement({ 15 | actions: [ 16 | 'dynamodb:PutItem', 17 | 'dynamodb:UpdateItem', 18 | 'dynamodb:GetItem', 19 | 'dynamodb:Query', 20 | 'dynamodb:DeleteItem', 21 | ], 22 | resources: [arn], 23 | }) 24 | 25 | export const DynamoDbReadOnly = (arn: string): iam.PolicyStatement => 26 | new iam.PolicyStatement({ 27 | actions: [ 28 | 'dynamodb:GetItem', 29 | 'dynamodb:Query', 30 | ], 31 | resources: [arn], 32 | }) -------------------------------------------------------------------------------- /cloud/src/storage.ts: -------------------------------------------------------------------------------- 1 | import * as ddb from '@aws-cdk/aws-dynamodb' 2 | import * as cdk from '@aws-cdk/core' 3 | import * as pure from 'aws-cdk-pure' 4 | 5 | export const DDB = (vsn: string): pure.IPure => { 6 | const iaac = pure.iaac(ddb.Table) 7 | const AuthPubKey = (): ddb.TableProps => ({ 8 | partitionKey: {type: ddb.AttributeType.STRING, name: 'prefix'}, 9 | readCapacity: 1, 10 | removalPolicy: vsn.startsWith('pr') ? cdk.RemovalPolicy.DESTROY : cdk.RemovalPolicy.RETAIN, 11 | sortKey: {type: ddb.AttributeType.STRING, name: 'suffix'}, 12 | tableName: `${cdk.Aws.STACK_NAME}-pubkey`, 13 | writeCapacity: 1, 14 | }) 15 | return iaac(AuthPubKey) 16 | } 17 | -------------------------------------------------------------------------------- /cloud/src/token.ts: -------------------------------------------------------------------------------- 1 | import * as api from '@aws-cdk/aws-apigateway' 2 | import * as ddb from '@aws-cdk/aws-dynamodb' 3 | import * as iam from '@aws-cdk/aws-iam' 4 | import * as lambda from '@aws-cdk/aws-lambda' 5 | import * as logs from '@aws-cdk/aws-logs' 6 | import * as cdk from '@aws-cdk/core' 7 | import * as pure from 'aws-cdk-pure' 8 | import * as permit from './permit' 9 | 10 | // 11 | // 12 | export const Function = (host: string, db: ddb.Table, layers: lambda.ILayerVersion[]): pure.IPure => 13 | pure.wrap(api.LambdaIntegration)( 14 | Role(db).flatMap(x => Lambda(host, db, x, layers)) 15 | ) 16 | 17 | // 18 | const Lambda = (host: string, db: ddb.Table, role: iam.IRole, layers: lambda.ILayerVersion[]): pure.IPure => { 19 | const iaac = pure.iaac(lambda.Function) 20 | const Token = (): lambda.FunctionProps => ({ 21 | code: new lambda.AssetCode('../apps/token/_build/default/bin'), 22 | environment: { 23 | 'PERMIT_AUDIENCE': 'any', 24 | 'PERMIT_CLAIMS': '', 25 | 'PERMIT_ISSUER': `https://${host}`, 26 | 'PERMIT_KEYPAIR': 'permit_config_ddb', 27 | 'PERMIT_STORAGE': `ddb+https://dynamodb.${cdk.Aws.REGION}.amazonaws.com:443/${db.tableName}` 28 | }, 29 | handler: 'index.main', 30 | layers, 31 | logRetention: logs.RetentionDays.FIVE_DAYS, 32 | memorySize: 256, 33 | reservedConcurrentExecutions: 5, 34 | role, 35 | runtime: lambda.Runtime.PROVIDED, 36 | timeout: cdk.Duration.seconds(10), 37 | }) 38 | return iaac(Token) 39 | } 40 | 41 | // 42 | const Role = (db: ddb.Table): pure.IPure => { 43 | const role = pure.iaac(iam.Role) 44 | const TokenRole = (): iam.RoleProps => ({ 45 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com') 46 | }) 47 | 48 | return role(TokenRole).effect(x => { 49 | x.addToPolicy(permit.LambdaLogging()) 50 | x.addToPolicy(permit.DynamoDbReadOnly(db.tableArn)) 51 | }) 52 | } -------------------------------------------------------------------------------- /cloud/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017.object", "es2017.string"], 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "strictNullChecks": true, 9 | "noImplicitThis": true, 10 | "alwaysStrict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": false, 15 | "inlineSourceMap": true, 16 | "inlineSources": true, 17 | "experimentalDecorators": true, 18 | "strictPropertyInitialization":false 19 | }, 20 | "include": ["src"], 21 | "exclude": ["node_modules"] 22 | } -------------------------------------------------------------------------------- /cloud/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name" : [true, "never-prefix"], 5 | "max-classes-per-file": [true, 2], 6 | "no-unused-expression": ["allow-new"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /doc/install.md: -------------------------------------------------------------------------------- 1 | # Install OAuth2 appliance 2 | 3 | 4 | ## AWS Elastic Container Service 5 | 6 | The application installation requires 7 | * AWS account 8 | * [AWS command-line tools](https://aws.amazon.com/cli/) 9 | * [AWS ECS command-line tools](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_CLI.html) 10 | * Valid access right to execute `aws` utility. 11 | 12 | ### Provision backing service 13 | 14 | The appliance uses concept of backing service to attach all resource required for operation. It provides Infrastructure-as-a-Code templates to spawn required cloud resources and treats them as backing services. 15 | 16 | > A backing service is any service the app consumes over the network as part of its normal operation. 17 | 18 | Use the following command to spawn all required service. The script uses supplied aws cloud formation [template](../rel/aws/resources.yml). As the result, you'll get a new stack `${Env}-oauth2-resources` in your console. 19 | 20 | ``` 21 | make config-aws ENV=live 22 | ``` 23 | 24 | ### Configure OAuth2 25 | 26 | OAuth2 deployment requires a configuration to address you needs. Use the [compose file]((../rel/aws/compose.yml)) as a template. 27 | 28 | ```bash 29 | ## Enable DynamoDB as storage back-end, 30 | ## You might need to configure AWS Region only 31 | OAUTH2_STORAGE=ddb+https://dynamodb.eu-west-1.amazonaws.com:443/live-oauth2-pubkey?hashkey=access 32 | 33 | ## Server port and protocol, see Enable Secure Transport Layer if you need https 34 | OAUTH2_PORT=http://*:8080 35 | OAUTH2_TLS_CERTIFICATE=none 36 | OAUTH2_TLS_PRIVATE_KEY=none 37 | 38 | ## define claim issuer 39 | ## RFC 7519 40 | ## 4.1.1. "iss" (Issuer) Claim 41 | OAUTH2_ISSUER=http://localhost:8080 42 | 43 | ## define claim audience 44 | ## RFC 7519 45 | ## 4.1.3. "aud" (Audience) Claim 46 | OAUTH2_AUDIENCE=any 47 | 48 | ## comma separated list of default claim scopes 49 | OAUTH2_CLAIMS=uid=true 50 | 51 | ## 52 | ## configure token(s) time-to-live in seconds 53 | OAUTH2_TTL_ACCESS_TOKEN=3600 54 | OAUTH2_TTL_REFRESH_TOKEN=86400 55 | OAUTH2_TTL_EXCHANGE_CODE=60 56 | ``` 57 | 58 | #### Enable GitHub integration 59 | 60 | OAuth2 supports integration with GitHub. This feature is a classical account federation. See the developer guidelines for [details](https://developer.github.com/apps/building-oauth-apps/). Once, you've enabled app at GitHub console, you can configure this server 61 | 62 | ```bash 63 | GITHUB_ACCESS_KEY=YourAccessKey 64 | GITHUB_SECRET_KEY=YourSecretKey 65 | GITHUB_ORG=YourGitHubOrg 66 | ``` 67 | 68 | ### Spawn OAuth2 service 69 | 70 | The appliance supplies configuration to spawn service at AWS ECS. Just tell the cluster id at `ECS` variable and command will spawn the service 71 | 72 | ``` 73 | make service-up ENV=live ECS=mycluster 74 | ``` 75 | 76 | ### Enables Secure Transport Layer 77 | 78 | The usage of AWS ALB is the recommended approach to host OAuth2. However, this is becomes an overhead for extremely small projects. This authority server supports https protocol. Just change a protocol schema and provides certificates and private key. Note: only S3 as key source is supported. Easiest way to obtain trusted TLS certificate are [Let's Encrypt](https://letsencrypt.org) or [certbot-on-aws](https://github.com/fogfish/certbot-on-aws). 79 | 80 | ``` 81 | OAUTH2_PORT=https://*:8443 82 | OAUTH2_TLS_CERTIFICATE=s3://letsencrypt/my.domain/cert.pem 83 | OAUTH2_TLS_PRIVATE_KEY=s3://letsencrypt/my.domain/privkey.pem 84 | ``` 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /doc/restapi.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: v2.0 4 | title: OAuth 2.0 Authorization Server Interface 5 | description: 6 | The interface implements an authorization layer and separating the role of 7 | the client from that of the resource owner. 8 | 9 | See The OAuth 2.0 Authorization Framework and other related specifications 10 | 11 | * https://tools.ietf.org/html/rfc6749 12 | 13 | * https://tools.ietf.org/html/rfc8252 14 | 15 | * https://tools.ietf.org/html/rfc7662 16 | 17 | * https://tools.ietf.org/html/rfc7519 18 | 19 | 20 | This document quotes listed specification to clarify details of interfaces. 21 | 22 | ##----------------------------------------------------------------------------- 23 | ## 24 | ## Interface end-points 25 | ## 26 | ##----------------------------------------------------------------------------- 27 | 28 | paths: 29 | 30 | ## 31 | ## 32 | /oauth2/authorize: 33 | get: 34 | description: | 35 | The authorization endpoint is used to interact with the resource owner 36 | and obtain an authorization grant. The OAuth app redirects a consumer's 37 | user agent to initiate either Authorization Code or Implicit Grants. 38 | 39 | See RFC 6749 for details 40 | * https://tools.ietf.org/html/rfc6749#section-4.1 41 | * https://tools.ietf.org/html/rfc6749#section-4.2 42 | 43 | The endpoint renders a user-expereince that verifies the identity of 44 | the resource owner. It uses a traditional username/password pair to 45 | authenticate the resources owners. 46 | 47 | After completing its interaction with the resource owner, the authorization 48 | server directs the resource owner's user-agent back to the client. 49 | The authorization server redirects the user-agent to the client's 50 | redirection endpoint previously established with the authorization 51 | server during the client registration process. 52 | 53 | 54 | parameters: 55 | - $ref: '#/components/parameters/client_id' 56 | - $ref: '#/components/parameters/response_type' 57 | - $ref: '#/components/parameters/state' 58 | 59 | responses: 60 | '200': 61 | description: | 62 | Renders OAuth 2.0 user expereince. 63 | 64 | content: 65 | text/html: 66 | schema: 67 | type: string 68 | format: binary 69 | 70 | default: 71 | description: | 72 | Unable to execute the request 73 | content: 74 | application/json: 75 | schema: 76 | $ref: '#/components/schemas/error' 77 | 78 | ## 79 | ## 80 | /oauth2/token: 81 | post: 82 | description: | 83 | The token endpoint is used by the client to obtain an access token by 84 | presenting its authorization grant or refresh token. 85 | 86 | requestBody: 87 | required: true 88 | content: 89 | application/x-www-form-urlencoded: 90 | schema: 91 | $ref: '#/components/schemas/request' 92 | 93 | responses: 94 | '200': 95 | description: | 96 | Authorization server exchanges the grant to valid token. 97 | 98 | content: 99 | application/json: 100 | schema: 101 | $ref: '#/components/schemas/token' 102 | 103 | default: 104 | description: | 105 | Unable to execute the request 106 | content: 107 | application/json: 108 | schema: 109 | $ref: '#/components/schemas/error' 110 | 111 | ## 112 | ## 113 | /oauth2/introspect: 114 | post: 115 | description: | 116 | The contents of access tokens are opaque to clients. The client does not 117 | need to know anything about the content or structure of the token itself, 118 | if there is any. However, there is still a large amount of metadata that 119 | may be attached to a token, such as its current validity, approved scopes, 120 | and information about the context in which the token was issued. These pieces 121 | of information are often vital to protected resources making authorization 122 | decisions based on the tokens being presented. 123 | 124 | This specification defines a protocol that allows authorized protected 125 | resources to query the authorization server to determine the set of metadata 126 | for a given token that was presented to them by an OAuth 2.0 client. 127 | 128 | See https://tools.ietf.org/html/rfc7662 129 | 130 | requestBody: 131 | required: true 132 | content: 133 | application/x-www-form-urlencoded: 134 | schema: 135 | type: object 136 | properties: 137 | token: 138 | type: string 139 | description: | 140 | access token issued by the service 141 | 142 | responses: 143 | '200': 144 | description: | 145 | Authorization server return claims assotiated with the token. 146 | 147 | content: 148 | application/json: 149 | schema: 150 | $ref: '#/components/schemas/claims' 151 | 152 | default: 153 | description: | 154 | Unable to execute the request 155 | content: 156 | application/json: 157 | schema: 158 | $ref: '#/components/schemas/error' 159 | 160 | ## 161 | ## 162 | /oauth2/jwks: 163 | get: 164 | description: | 165 | The endpoint returns a public key used to sign access tokens. 166 | 167 | responses: 168 | '200': 169 | description: | 170 | Authorization server return claims assotiated with the token. 171 | 172 | content: 173 | application/json: 174 | schema: 175 | $ref: '#/components/schemas/jwks' 176 | 177 | default: 178 | description: | 179 | Unable to execute the request 180 | content: 181 | application/json: 182 | schema: 183 | $ref: '#/components/schemas/error' 184 | 185 | ## 186 | ## 187 | /oauth2/signin: 188 | post: 189 | description: | 190 | Request the consumer sign-in with username/password credentials. 191 | 192 | Note: this api is not defined by OAuth 2.0 specification, it is defined 193 | by this authorization server to facilitate an implementation of 194 | branded identity management process. 195 | 196 | requestBody: 197 | required: true 198 | content: 199 | application/x-www-form-urlencoded: 200 | schema: 201 | $ref: '#/components/schemas/identity' 202 | 203 | responses: 204 | '302': 205 | description: | 206 | The authorization server validates credentials and redirects 207 | consumer's user-agent to redirect uri of the client who issues 208 | the request. 209 | 210 | default: 211 | description: | 212 | Unable to execute the request 213 | content: 214 | application/json: 215 | schema: 216 | $ref: '#/components/schemas/error' 217 | 218 | 219 | /oauth2/signup: 220 | post: 221 | description: | 222 | Request the consumer sign-up with username/password credentials. 223 | 224 | Note: this api is not defined by OAuth 2.0 specification, it is defined 225 | by this authorization server to facilitate an implementation of 226 | branded identity management process. 227 | 228 | requestBody: 229 | required: true 230 | content: 231 | application/x-www-form-urlencoded: 232 | schema: 233 | $ref: '#/components/schemas/identity' 234 | 235 | responses: 236 | '302': 237 | description: | 238 | The authorization server validates credentials, creates a basic 239 | profile and redirects consumer's user-agent to redirect uri of 240 | the client who issues the request. 241 | 242 | default: 243 | description: | 244 | Unable to execute the request 245 | content: 246 | application/json: 247 | schema: 248 | $ref: '#/components/schemas/error' 249 | 250 | components: 251 | 252 | ##----------------------------------------------------------------------------- 253 | ## 254 | ## Request parameters 255 | ## 256 | ##----------------------------------------------------------------------------- 257 | parameters: 258 | 259 | ## 260 | client_id: 261 | name: client_id 262 | in: query 263 | description: | 264 | The valid client identifier issued by the authorization server 265 | See https://tools.ietf.org/html/rfc6749#section-2.2 266 | required: true 267 | schema: 268 | type: string 269 | 270 | ## 271 | response_type: 272 | name: response_type 273 | in: query 274 | description: | 275 | The client informs the authorization server of the desired grant type 276 | See https://tools.ietf.org/html/rfc6749#section-3.1.1 277 | required: true 278 | schema: 279 | type: string 280 | 281 | ## 282 | state: 283 | name: state 284 | in: query 285 | description: | 286 | An opaque value used by the client to maintain state between the request and callback. 287 | required: true 288 | schema: 289 | type: string 290 | 291 | 292 | ##----------------------------------------------------------------------------- 293 | ## 294 | ## Data Types (Schemas) definition 295 | ## 296 | ##----------------------------------------------------------------------------- 297 | schemas: 298 | 299 | ## 300 | ## 301 | identity: 302 | type: object 303 | description: | 304 | The class exposes consumer and client identity required for sign-in, sign-up 305 | procedures. 306 | required: 307 | - access 308 | - secret 309 | - response_type 310 | - client_id 311 | - state 312 | properties: 313 | access: 314 | type: string 315 | description: | 316 | the access key is a public identity of the principal to whom access is granted. 317 | secret: 318 | type: string 319 | description: | 320 | the secret key is a private identity of the principal to whom access is granted. 321 | response_type: 322 | type: string 323 | description: | 324 | The client informs the authorization server of the desired grant type 325 | See https://tools.ietf.org/html/rfc6749#section-3.1.1 326 | client_id: 327 | type: string 328 | description: | 329 | The valid client identifier issued by the authorization server 330 | See https://tools.ietf.org/html/rfc6749#section-2.2 331 | state: 332 | type: string 333 | description: | 334 | An opaque value used by the client to maintain state between the request and callback. 335 | 336 | ## 337 | ## 338 | request: 339 | type: object 340 | description: | 341 | The data type represent the access token request 342 | * Authorization Code Grant https://tools.ietf.org/html/rfc6749#section-4.1.3 343 | * Resource Owner Password Credentials Grant https://tools.ietf.org/html/rfc6749#section-4.3.2 344 | * Client Credentials Grant https://tools.ietf.org/html/rfc6749#section-4.4.2 345 | * Refreshing access token https://tools.ietf.org/html/rfc6749#section-6 346 | required: 347 | - grant_type 348 | properties: 349 | grant_type: 350 | type: string 351 | description: | 352 | the type of grant 353 | code: 354 | type: string 355 | description: | 356 | The authorization code received from the authorization server. 357 | username: 358 | type: string 359 | description: | 360 | The resource owner username. 361 | password: 362 | type: string 363 | description: | 364 | The resource owner password. 365 | refresh_token: 366 | type: string 367 | description: | 368 | The refresh token issued to the client. 369 | scopes: 370 | type: string 371 | description: | 372 | The scope of the access request as described by https://tools.ietf.org/html/rfc6749#section-3.3 373 | 374 | ## 375 | ## 376 | token: 377 | type: object 378 | description: | 379 | The authorization server issues an access token and optional refresh token. 380 | https://tools.ietf.org/html/rfc6749#section-5.1 381 | required: 382 | - access_token 383 | - token_type 384 | - expires_in 385 | properties: 386 | access_token: 387 | type: string 388 | description: | 389 | The access token issued by the authorization server. 390 | token_type: 391 | type: string 392 | description: | 393 | The type of the token issued (usually bearer) 394 | expires_in: 395 | type: string 396 | description: | 397 | The lifetime in seconds of the access token. 398 | refresh_token: 399 | type: string 400 | description: | 401 | The refresh token, which can be used to obtain new access tokens. 402 | scopes: 403 | type: string 404 | description: | 405 | The scope of the access request as described by https://tools.ietf.org/html/rfc6749#section-3.3 406 | 407 | ## 408 | ## 409 | claims: 410 | type: object 411 | description: | 412 | It represent a set of security claims as a JSON object 413 | required: 414 | - iss 415 | - aud 416 | - sub 417 | - idp 418 | - app 419 | properties: 420 | iss: 421 | type: string 422 | description: | 423 | identifies the security token service (STS) that constructs and returns the token. 424 | aud: 425 | type: string 426 | description: | 427 | identifies intended recipient of the token. The application that receives the token 428 | must verify that the audience value is correct and reject any tokens intended for 429 | a different audience. 430 | sub: 431 | type: string 432 | description: | 433 | identifies the principal about which the token asserts information, such as 434 | the user of an application. 435 | idp: 436 | type: string 437 | description: | 438 | records the identity provider that authenticated the subject of the token. 439 | app: 440 | type: string 441 | description: | 442 | identifies the application that is using the token to access a resource. 443 | rev: 444 | type: string 445 | description: | 446 | identifies if token is revocable 447 | 448 | ## 449 | ## 450 | jwks: 451 | type: array 452 | items: 453 | $ref: "#/components/schemas/jwk" 454 | 455 | jwk: 456 | type: object 457 | required: 458 | - alg 459 | - kty 460 | - kid 461 | - e 462 | - n 463 | properties: 464 | alg: 465 | type: string 466 | description: | 467 | the algorithm for the key 468 | kty: 469 | type: string 470 | description: | 471 | the key type 472 | kid: 473 | type: string 474 | description: | 475 | the unique identifier for the key 476 | e: 477 | type: string 478 | description: | 479 | the exponent for a standard pem 480 | n: 481 | type: string 482 | description: | 483 | the modulus for a standard pem 484 | 485 | ## 486 | ## 487 | error: 488 | description: | 489 | The high-level class to expose interface errors (See RFC 7807). 490 | required: 491 | - type 492 | - status 493 | - title 494 | - instance 495 | - details 496 | properties: 497 | type: 498 | type: string 499 | description: | 500 | A URI reference that identifies the problem type. 501 | status: 502 | type: number 503 | format: integer 504 | description: | 505 | The HTTP status code generated by the interface 506 | title: 507 | type: string 508 | description: | 509 | A short, human-readable summary of the problem type. 510 | instance: 511 | type: string 512 | description: | 513 | A URI reference that identifies faulty resource and its context. 514 | details: 515 | type: string 516 | description: | 517 | A human-readable explanation specific to this occurrence of the problem. 518 | -------------------------------------------------------------------------------- /doc/security.md: -------------------------------------------------------------------------------- 1 | # Security concerns 2 | 3 | The [OAuth 2.0 Framework](https://tools.ietf.org/html/rfc6749) provides security guidelines for implementers. This sections uses a FAQ style (quote - answer) to explains how these security concerns has been addressed by the OAuth 2.0 appliance. 4 | 5 | ## Client Authentication 6 | 7 | > The authorization server establishes client credentials with web application clients for the purpose of client authentication. The authorization server is encouraged to consider stronger client authentication means than a client password. Web application clients MUST ensure confidentiality of client passwords and other client credentials. 8 | 9 | > The authorization server MUST NOT issue client passwords or other client credentials to native application or user-agent-based application clients for the purpose of client authentication. The authorization server MAY issue a client password or other credentials for a specific installation of a native application client on a specific device. 10 | 11 | > When client authentication is not possible, the authorization server SHOULD employ other means to validate the client's identity -- for example, by requiring the registration of the client redirection URI or enlisting the resource owner to confirm identity. A valid redirection URI is not sufficient to verify the client's identity when asking for resource owner authorization but can be used to prevent delivering credentials to a counterfeit client after obtaining resource owner authorization. 12 | 13 | > The authorization server must consider the security implications of interacting with unauthenticated clients and take measures to limit the potential exposure of other credentials (e.g., refresh tokens) issued to such clients. 14 | 15 | ## Client Impersonation 16 | 17 | > A malicious client can impersonate another client and obtain access to protected resources if the impersonated client fails to, or is unable to, keep its client credentials confidential. 18 | 19 | > The authorization server MUST authenticate the client whenever possible. If the authorization server cannot authenticate the client due to the client's nature, the authorization server MUST require the registration of any redirection URI used for receiving authorization responses and SHOULD utilize other means to protect resource owners from such potentially malicious clients. For example, the authorization server can engage the resource owner to assist in identifying the client and its origin. 20 | 21 | > The authorization server SHOULD enforce explicit resource owner authentication and provide the resource owner with information about the client and the requested authorization scope and lifetime. It is up to the resource owner to review the information in the context of the current client and to authorize or deny the request. 22 | 23 | > The authorization server SHOULD NOT process repeated authorization requests automatically (without active resource owner interaction) without authenticating the client or relying on other measures to ensure that the repeated request comes from the original client and not an impersonator. 24 | 25 | ## Access Tokens 26 | 27 | > Access token credentials (as well as any confidential access token attributes) MUST be kept confidential in transit and storage, and only shared among the authorization server, the resource servers the access token is valid for, and the client to whom the access token is issued. Access token credentials MUST only be transmitted using TLS as described in Section 1.6 with server authentication as defined by [RFC2818]. 28 | 29 | > When using the implicit grant type, the access token is transmitted in the URI fragment, which can expose it to unauthorized parties. 30 | 31 | > The authorization server MUST ensure that access tokens cannot be generated, modified, or guessed to produce valid access tokens by unauthorized parties. 32 | 33 | > The client SHOULD request access tokens with the minimal scope necessary. The authorization server SHOULD take the client identity into account when choosing how to honor the requested scope and MAY issue an access token with less rights than requested. 34 | 35 | > This specification does not provide any methods for the resource server to ensure that an access token presented to it by a given client was issued to that client by the authorization server. 36 | 37 | ## Refresh Tokens 38 | 39 | > Authorization servers MAY issue refresh tokens to web application clients and native application clients. 40 | 41 | > Refresh tokens MUST be kept confidential in transit and storage, and shared only among the authorization server and the client to whom the refresh tokens were issued. The authorization server MUST maintain the binding between a refresh token and the client to whom it was issued. Refresh tokens MUST only be transmitted using TLS as described in Section 1.6 with server authentication as defined by [RFC2818]. 42 | 43 | > The authorization server MUST verify the binding between the refresh token and client identity whenever the client identity can be authenticated. When client authentication is not possible, the authorization server SHOULD deploy other means to detect refresh token abuse. 44 | 45 | > For example, the authorization server could employ refresh token rotation in which a new refresh token is issued with every access token refresh response. The previous refresh token is invalidated but retained by the authorization server. If a refresh token is compromised and subsequently used by both the attacker and the legitimate client, one of them will present an invalidated refresh token, which will inform the authorization server of the breach. 46 | 47 | > The authorization server MUST ensure that refresh tokens cannot be generated, modified, or guessed to produce valid refresh tokens by unauthorized parties. 48 | 49 | 50 | ## Authorization Codes 51 | 52 | > The transmission of authorization codes SHOULD be made over a secure channel, and the client SHOULD require the use of TLS with its redirection URI if the URI identifies a network resource. Since authorization codes are transmitted via user-agent redirections, they could potentially be disclosed through user-agent history and HTTP referrer headers. 53 | 54 | > Authorization codes operate as plaintext bearer credentials, used to verify that the resource owner who granted authorization at the authorization server is the same resource owner returning to the client to complete the process. Therefore, if the client relies on the authorization code for its own resource owner authentication, the client redirection endpoint MUST require the use of TLS. 55 | 56 | > Authorization codes MUST be short lived and single-use. If the authorization server observes multiple attempts to exchange an authorization code for an access token, the authorization server SHOULD attempt to revoke all access tokens already granted based on the compromised authorization code. 57 | 58 | > If the client can be authenticated, the authorization servers MUST authenticate the client and ensure that the authorization code was issued to the same client. 59 | 60 | ## Authorization Code Redirection URI Manipulation 61 | 62 | > When requesting authorization using the authorization code grant type, the client can specify a redirection URI via the "redirect_uri" parameter. If an attacker can manipulate the value of the redirection URI, it can cause the authorization server to redirect the resource owner user-agent to a URI under the control of the attacker with the authorization code. 63 | 64 | > An attacker can create an account at a legitimate client and initiate the authorization flow. When the attacker's user-agent is sent to the authorization server to grant access, the attacker grabs the authorization URI provided by the legitimate client and replaces the client's redirection URI with a URI under the control of the attacker. The attacker then tricks the victim into following the manipulated link to authorize access to the legitimate client. 65 | 66 | > Once at the authorization server, the victim is prompted with a normal, valid request on behalf of a legitimate and trusted client, and authorizes the request. The victim is then redirected to an endpoint under the control of the attacker with the authorization code. The attacker completes the authorization flow by sending the authorization code to the client using the original redirection URI provided by the client. The client exchanges the authorization code with an access token and links it to the attacker's client account, which can now gain access to the protected resources authorized by the victim (via the client). 67 | 68 | > In order to prevent such an attack, the authorization server MUST ensure that the redirection URI used to obtain the authorization code is identical to the redirection URI provided when exchanging the authorization code for an access token. The authorization server MUST require public clients and SHOULD require confidential clients to register their redirection URIs. If a redirection URI is provided in the request, the authorization server MUST validate it against the registered value. 69 | 70 | ## Resource Owner Password Credentials 71 | 72 | > The resource owner password credentials grant type is often used for legacy or migration reasons. It reduces the overall risk of storing usernames and passwords by the client but does not eliminate the need to expose highly privileged credentials to the client. 73 | 74 | > This grant type carries a higher risk than other grant types because it maintains the password anti-pattern this protocol seeks to avoid. The client could abuse the password, or the password could unintentionally be disclosed to an attacker (e.g., via log files or other records kept by the client). 75 | 76 | > Additionally, because the resource owner does not have control over the authorization process (the resource owner's involvement ends when it hands over its credentials to the client), the client can obtain access tokens with a broader scope than desired by the resource owner. The authorization server should consider the scope and lifetime of access tokens issued via this grant type. 77 | 78 | > The authorization server and client SHOULD minimize use of this grant type and utilize other grant types whenever possible. 79 | 80 | ## Request Confidentiality 81 | 82 | > Access tokens, refresh tokens, resource owner passwords, and client credentials MUST NOT be transmitted in the clear. Authorization codes SHOULD NOT be transmitted in the clear. 83 | 84 | > The "state" and "scope" parameters SHOULD NOT include sensitive client or resource owner information in plain text, as they can be transmitted over insecure channels or stored insecurely. 85 | 86 | ## Ensuring Endpoint Authenticity 87 | 88 | > In order to prevent man-in-the-middle attacks, the authorization server MUST require the use of TLS with server authentication as defined by [RFC2818] for any request sent to the authorization and token endpoints. The client MUST validate the authorization server's TLS certificate as defined by [RFC6125] and in accordance with its requirements for server identity authentication. 89 | 90 | 91 | ## Credentials-Guessing Attacks 92 | 93 | > The authorization server MUST prevent attackers from guessing access tokens, authorization codes, refresh tokens, resource owner passwords, and client credentials. 94 | 95 | > The probability of an attacker guessing generated tokens (and other credentials not intended for handling by end-users) MUST be less than or equal to 2^(-128) and SHOULD be less than or equal to 2^(-160). 96 | 97 | > The authorization server MUST utilize other means to protect credentials intended for end-user usage. 98 | 99 | 100 | ## Phishing Attacks 101 | 102 | > Wide deployment of this and similar protocols may cause end-users to become inured to the practice of being redirected to websites where they are asked to enter their passwords. If end-users are not careful to verify the authenticity of these websites before entering their credentials, it will be possible for attackers to exploit this practice to steal resource owners' passwords. 103 | 104 | > Service providers should attempt to educate end-users about the risks phishing attacks pose and should provide mechanisms that make it easy for end-users to confirm the authenticity of their sites. Client developers should consider the security implications of how they interact with the user-agent (e.g., external, embedded), and the ability of the end-user to verify the authenticity of the authorization server. 105 | 106 | > To reduce the risk of phishing attacks, the authorization servers MUST require the use of TLS on every endpoint used for end-user interaction. 107 | 108 | ## Cross-Site Request Forgery 109 | 110 | TBD 111 | 112 | ## Clickjacking 113 | 114 | > In a clickjacking attack, an attacker registers a legitimate client and then constructs a malicious site in which it loads the authorization server's authorization endpoint web page in a transparent iframe overlaid on top of a set of dummy buttons, which are carefully constructed to be placed directly under important buttons on the authorization page. When an end-user clicks a misleading visible button, the end-user is actually clicking an invisible button on the authorization page (such as an "Authorize" button). This allows an attacker to trick a resource owner into granting its client access without the end-user's knowledge. 115 | 116 | > To prevent this form of attack, native applications SHOULD use external browsers instead of embedding browsers within the application when requesting end-user authorization. For most newer browsers, avoidance of iframes can be enforced by the authorization server using the (non-standard) "x-frame-options" header. This header can have two values, "deny" and "sameorigin", which will block any framing, or framing by sites with a different origin, respectively. For older browsers, JavaScript frame-busting techniques can be used but may not be effective in all browsers. 117 | 118 | ## Code Injection and Input Validation 119 | 120 | > A code injection attack occurs when an input or otherwise external variable is used by an application unsanitized and causes modification to the application logic. This may allow an attacker to gain access to the application device or its data, cause denial of service, or introduce a wide range of malicious side-effects. 121 | 122 | > The authorization server and client MUST sanitize (and validate when possible) any value received -- in particular, the value of the "state" and "redirect_uri" parameters. 123 | 124 | ## Open Redirectors 125 | 126 | > The authorization server, authorization endpoint, and client redirection endpoint can be improperly configured and operate as open redirectors. An open redirector is an endpoint using a parameter to automatically redirect a user-agent to the location specified by the parameter value without any validation. 127 | 128 | > Open redirectors can be used in phishing attacks, or by an attacker to get end-users to visit malicious sites by using the URI authority component of a familiar and trusted destination. In addition, if the authorization server allows the client to register only part of the redirection URI, an attacker can use an open redirector operated by the client to construct a redirection URI that will pass the authorization server validation but will send the authorization code or access token to an endpoint under the control of the attacker. 129 | 130 | ## Misuse of Access Token to Impersonate Resource Owner in Implicit Flow 131 | 132 | > For public clients using implicit flows, this specification does not provide any method for the client to determine what client an access token was issued to. 133 | 134 | > A resource owner may willingly delegate access to a resource by granting an access token to an attacker's malicious client. This may be due to phishing or some other pretext. An attacker may also steal a token via some other mechanism. An attacker may then attempt to impersonate the resource owner by providing the access token to a legitimate public client. 135 | 136 | > In the implicit flow (response_type=token), the attacker can easily switch the token in the response from the authorization server, replacing the real access token with the one previously issued to the attacker. 137 | 138 | > Servers communicating with native applications that rely on being passed an access token in the back channel to identify the user of the client may be similarly compromised by an attacker creating a compromised application that can inject arbitrary stolen access tokens. 139 | 140 | > Any public client that makes the assumption that only the resource owner can present it with a valid access token for the resource is vulnerable to this type of attack. 141 | 142 | > This type of attack may expose information about the resource owner at the legitimate client to the attacker (malicious client). This will also allow the attacker to perform operations at the legitimate client with the same permissions as the resource owner who originally granted the access token or authorization code. 143 | 144 | > Authenticating resource owners to clients is out of scope for this specification. Any specification that uses the authorization process as a form of delegated end-user authentication to the client (e.g., third-party sign-in service) MUST NOT use the implicit flow without additional security mechanisms that would enable the client to determine if the access token was issued for its use (e.g., audience-restricting the access token). 145 | 146 | 147 | ## JWT is broken 148 | 149 | http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/ 150 | https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid 151 | 152 | ## Redirect end-point content 153 | 154 | 3.1.2.5. Endpoint Content 155 | 156 | The redirection request to the client's endpoint typically results in 157 | an HTML document response, processed by the user-agent. If the HTML 158 | response is served directly as the result of the redirection request, 159 | any script included in the HTML document will execute with full 160 | access to the redirection URI and the credentials it contains. 161 | 162 | The client SHOULD NOT include any third-party scripts (e.g., third- 163 | party analytics, social plug-ins, ad networks) in the redirection 164 | endpoint response. Instead, it SHOULD extract the credentials from 165 | the URI and redirect the user-agent again to another endpoint without 166 | exposing the credentials (in the URI or elsewhere). If third-party 167 | scripts are included, the client MUST ensure that its own scripts 168 | (used to extract and remove the credentials from the URI) will 169 | execute first. 170 | 171 | -------------------------------------------------------------------------------- /js/account/.env: -------------------------------------------------------------------------------- 1 | ## 2 | ## app config 3 | ## 4 | REACT_APP_OAUTH2_AUTHORIZE=https://SITE/oauth2/authorize 5 | REACT_APP_OAUTH2_TOKEN=https://SITE/oauth2/token 6 | REACT_APP_OAUTH2_CLIENT=https://SITE/oauth2/client 7 | REACT_APP_OAUTH2_CLIENT_ID=account@oauth2 8 | REACT_APP_OAUTH2_FLOW_TYPE=code 9 | 10 | -------------------------------------------------------------------------------- /js/account/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true, 6 | "browser": true 7 | }, 8 | "settings": { 9 | "import/resolver": { 10 | "node": { 11 | "paths": ["./src", "./src/style"], 12 | "extensions": [".js", ".jsx"] 13 | } 14 | } 15 | }, 16 | "extends": [ 17 | "eslint:recommended", 18 | "plugin:react/recommended", 19 | "airbnb" 20 | ], 21 | "parserOptions": { 22 | "ecmaVersion": 2018 23 | }, 24 | "rules": { 25 | "max-classes-per-file": "off", 26 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 27 | "no-console": "error", 28 | "semi": "off", 29 | "react/prop-types": "off", 30 | "arrow-parens": ["error", "as-needed"], 31 | "react/jsx-props-no-spreading": ["error", { 32 | "exceptions": ["Component", "Recover", "Loading"] 33 | }], 34 | "camelcase": "off" 35 | } 36 | } -------------------------------------------------------------------------------- /js/account/Makefile: -------------------------------------------------------------------------------- 1 | ## @doc 2 | ## static site deployment with AWS Gateway 3 | .PHONY: all deps test clean dist dist-up dist-rm 4 | 5 | ## 6 | ## Global config 7 | APP ?= account 8 | VSN ?= $(shell test -z "`git status --porcelain`" && git describe --tags --long | sed -e 's/-g[0-9a-f]*//' | sed -e 's/-0//' || echo "`git describe --abbrev=0 --tags`-a") 9 | SITE ?= ${VSN}.auth.${CONFIG_DOMAIN} 10 | ROOT ?= /oauth2/account 11 | BUILD = build 12 | 13 | ## 14 | ## 15 | all: test 16 | 17 | deps: | node_modules 18 | 19 | test: deps 20 | npm run lint 21 | 22 | .env.production: .env 23 | cp $^ $@ 24 | sed -i 's/SITE/${SITE}/g' $@ 25 | 26 | node_modules: 27 | @npm install 28 | 29 | clean: 30 | -@rm .env.production 31 | -@rm -Rf build 32 | 33 | ## 34 | ## 35 | dist: deps .env.production 36 | @PUBLIC_URL=${ROOT} npm run build 37 | 38 | dist-up: 39 | @aws s3 sync ${BUILD} s3://${SITE}/${APP}/ --delete 40 | @find ${BUILD} -name "*.html" | cut -d'/' -f2- |\ 41 | xargs -L 1 -I {} aws s3 cp ${BUILD}/{} s3://${SITE}/${APP}/{} --cache-control "max-age=20" 42 | 43 | dist-rm: 44 | @aws s3 rm s3://${SITE}/${APP} --recursive 45 | -------------------------------------------------------------------------------- /js/account/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src" 4 | }, 5 | "exclude": ["node_modules", "build", "coverage", "dist", "lib"] 6 | } -------------------------------------------------------------------------------- /js/account/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2-account", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@blueprintjs/core": "^3.19.1", 7 | "@blueprintjs/icons": "^3.12.0", 8 | "query-string": "^6.9.0", 9 | "react": "^16.12.0", 10 | "react-dom": "^16.12.0", 11 | "react-hook-oauth2": "^0.1.0", 12 | "react-scripts": "3.2.0", 13 | "reflexbox": "^4.0.6" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject", 20 | "lint": "eslint src" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "eslint": "^6.7.1", 36 | "eslint-config-airbnb": "^18.0.1", 37 | "eslint-plugin-react": "^7.17.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /js/account/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogfish/oauth2/f758bf689833aa97947e7740e64cbb9211dfa5c2/js/account/public/favicon.ico -------------------------------------------------------------------------------- /js/account/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Account Settings 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /js/account/src/app.css: -------------------------------------------------------------------------------- 1 | @import "../node_modules/@blueprintjs/core/lib/css/blueprint.css"; 2 | 3 | body 4 | { 5 | background: #F5F8FA; 6 | margin: 0; 7 | } 8 | 9 | table.bp3-html-table 10 | { 11 | width: 100%; 12 | border-collapse: collapse; 13 | } 14 | 15 | @media only screen and 16 | (max-width: 980px), 17 | (min-device-width: 980px) and (max-device-width: 1024px) 18 | { 19 | table.bp3-html-table, 20 | table.bp3-html-table thead, 21 | table.bp3-html-table tbody, 22 | table.bp3-html-table th, 23 | table.bp3-html-table td, 24 | table.bp3-html-table tr { 25 | display: block; 26 | } 27 | 28 | table.bp3-html-table thead tr { 29 | display: none; 30 | } 31 | 32 | table.bp3-html-table tbody tr:first-child td { 33 | border: none; 34 | box-shadow: none; 35 | } 36 | 37 | table.bp3-html-table td:before { 38 | display: block; 39 | font-weight: bold; 40 | content: attr(data-col); 41 | } 42 | 43 | } 44 | 45 | table.bp3-html-table th, 46 | table.bp3-html-table td 47 | { 48 | vertical-align: middle; 49 | } 50 | 51 | input 52 | { 53 | width: 100%; 54 | } -------------------------------------------------------------------------------- /js/account/src/app.js: -------------------------------------------------------------------------------- 1 | import Account from './components/Account' 2 | import './app.css' 3 | 4 | export default Account 5 | -------------------------------------------------------------------------------- /js/account/src/components/Account/Account.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Flex, Box } from 'reflexbox' 3 | import { useOAuth2, WhileIO } from 'react-hook-oauth2' 4 | import Header from './Header' 5 | import Registry from '../Registry' 6 | import Issue from '../Issue' 7 | 8 | const IO = WhileIO(undefined, Issue, Registry) 9 | 10 | const UI = () => { 11 | const status = useOAuth2() 12 | return () 13 | } 14 | 15 | const Account = () => ( 16 | <> 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | 26 | export default Account 27 | -------------------------------------------------------------------------------- /js/account/src/components/Account/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Navbar, 4 | Alignment, 5 | Icon, 6 | Tooltip, 7 | Intent, 8 | } from '@blueprintjs/core' 9 | import { IconNames } from '@blueprintjs/icons' 10 | import { authorize, WhoIs } from 'react-hook-oauth2' 11 | 12 | const Account = () => WhoIs(({ sub }) => (<>{sub})) 13 | 14 | const Header = () => ( 15 | 16 | 17 | Account 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | 29 | export default Header 30 | -------------------------------------------------------------------------------- /js/account/src/components/Account/index.js: -------------------------------------------------------------------------------- 1 | import Account from './Account' 2 | 3 | export default Account 4 | -------------------------------------------------------------------------------- /js/account/src/components/Issue/Issue.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NonIdealState, Button, Intent } from '@blueprintjs/core' 3 | import { IconNames } from '@blueprintjs/icons' 4 | import { authorize } from 'react-hook-oauth2' 5 | 6 | const Unauthorized = () => ( 7 | Please Sign-In!} 12 | /> 13 | ) 14 | 15 | const NotFound = () => ( 16 | Unable to find requested file.

} 20 | /> 21 | ) 22 | 23 | const Unknown = ({ type, title }) => ( 24 | 29 | ) 30 | 31 | export default ({ status }) => { 32 | switch (status.reason.type) { 33 | case 'https://httpstatuses.com/401': 34 | return () 35 | 36 | case 'https://httpstatuses.com/404': 37 | return () 38 | 39 | default: 40 | return () 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /js/account/src/components/Issue/index.js: -------------------------------------------------------------------------------- 1 | import Issue from './Issue' 2 | 3 | export default Issue 4 | -------------------------------------------------------------------------------- /js/account/src/components/NewApp/KeyPair/KeyPair.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Button, 4 | Classes, 5 | Callout, 6 | Code, 7 | H5, 8 | Intent, 9 | } from '@blueprintjs/core' 10 | 11 | const Secret = ({ access, secret, hide }) => ( 12 | <> 13 |
14 | 15 | This is the  16 | only 17 |  time that the secret access keys can be viewed or downloaded. 18 | You cannot recover them later. However, you can create new access keys at any time. 19 | 20 | 21 | 22 |
Access Key
23 |

{access}

24 | 25 |
Secret Key
26 |

{secret}

27 |
28 |
29 | 30 |
31 |
32 | 38 |
39 |
40 | 41 | ) 42 | 43 | export default Secret 44 | -------------------------------------------------------------------------------- /js/account/src/components/NewApp/KeyPair/index.js: -------------------------------------------------------------------------------- 1 | import KeyPair from './KeyPair' 2 | 3 | export default KeyPair 4 | -------------------------------------------------------------------------------- /js/account/src/components/NewApp/NewApp.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Dialog } from '@blueprintjs/core' 3 | import { useSecureCreate, SUCCESS } from 'react-hook-oauth2' 4 | import KeyPair from './KeyPair' 5 | import Registrar from './Registrar' 6 | 7 | const OAUTH2_CLIENT = process.env.REACT_APP_OAUTH2_CLIENT 8 | const emptyApp = { identity: undefined, redirect_uri: undefined, security: 'public' } 9 | 10 | const NewApp = ({ registrar, showRegistrar, append }) => { 11 | const [app, update] = useState(emptyApp) 12 | const { status, commit } = useSecureCreate(OAUTH2_CLIENT) 13 | 14 | return ( 15 | showRegistrar(false)} 21 | > 22 | {(status instanceof SUCCESS) 23 | && ( 24 | { 28 | append({ access: status.content.access, ...app }) 29 | showRegistrar(false) 30 | commit(undefined) 31 | update(emptyApp) 32 | }} 33 | /> 34 | )} 35 | {!(status instanceof SUCCESS) 36 | && ( 37 | commit(app)} 42 | /> 43 | )} 44 | 45 | ) 46 | } 47 | 48 | export default NewApp 49 | -------------------------------------------------------------------------------- /js/account/src/components/NewApp/Registrar/Endpoint.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Label, Classes } from '@blueprintjs/core' 3 | 4 | const Endpoint = ({ app, update }) => ( 5 | 17 | ) 18 | 19 | export default Endpoint 20 | -------------------------------------------------------------------------------- /js/account/src/components/NewApp/Registrar/Identity.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Label, Classes } from '@blueprintjs/core' 3 | 4 | const Identity = ({ app, update }) => ( 5 | 17 | ) 18 | 19 | export default Identity 20 | -------------------------------------------------------------------------------- /js/account/src/components/NewApp/Registrar/Registrar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Button, 4 | Classes, 5 | Intent, 6 | Callout, 7 | Code, 8 | } from '@blueprintjs/core' 9 | import { PENDING, FAILURE } from 'react-hook-oauth2' 10 | import Identity from './Identity' 11 | import Endpoint from './Endpoint' 12 | import Security from './Security' 13 | 14 | const Registrar = ({ 15 | status, 16 | commit, 17 | app, 18 | update, 19 | }) => ( 20 | <> 21 |
22 |

23 | Registers the application with the authorization server. Define client type 24 | based on their ability to maintain the confidentiality of their client credentials. 25 | The authorization server issues the registered client a client identifier and client secret. 26 |

27 | 28 | {(status instanceof FAILURE && status.reason.details !== 'invalid_uri') 29 | && ( 30 | 31 | Unable to register an application. Try again later. 32 | 33 | )} 34 | 35 | 36 | {(status instanceof FAILURE && status.reason.details === 'invalid_uri') 37 | && ( 38 | 39 | Invalid  40 | Redirect Uri 41 | . The schema, host and path are required. 42 | https://example.com/path 43 | 44 | )} 45 | 46 | 47 |
48 |
49 |
50 | 57 |
58 |
59 | 60 | ) 61 | 62 | export default Registrar 63 | -------------------------------------------------------------------------------- /js/account/src/components/NewApp/Registrar/Security.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Label, RadioGroup, Radio } from '@blueprintjs/core' 3 | 4 | const Security = ({ app, update }) => ( 5 | 19 | ) 20 | 21 | export default Security 22 | -------------------------------------------------------------------------------- /js/account/src/components/NewApp/Registrar/index.js: -------------------------------------------------------------------------------- 1 | import Registrar from './Registrar' 2 | 3 | export default Registrar 4 | -------------------------------------------------------------------------------- /js/account/src/components/NewApp/index.js: -------------------------------------------------------------------------------- 1 | import NewApp from './NewApp' 2 | 3 | export default NewApp 4 | -------------------------------------------------------------------------------- /js/account/src/components/Registry/Registry.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { 3 | Card, 4 | H2, 5 | Button, 6 | Intent, 7 | Spinner, 8 | } from '@blueprintjs/core' 9 | import { WhileIO, SUCCESS, useSecureLookup } from 'react-hook-oauth2' 10 | import Issue from '../Issue' 11 | import RegistryNone from './RegistryNone' 12 | import RegistryList from './RegistryList' 13 | import NewApp from '../NewApp' 14 | 15 | const OAUTH2_CLIENT = process.env.REACT_APP_OAUTH2_CLIENT 16 | 17 | // 18 | const Head = ({ status, showRegistrar }) => ( 19 |

20 | OAuth Apps 21 | {status instanceof SUCCESS 22 | && ( 23 | 31 | )} 32 |

33 | ) 34 | 35 | const Registry = ({ content, revoke, showRegistrar }) => (content && content.length > 0 36 | ? 37 | : 38 | ) 39 | 40 | const IO = WhileIO(Spinner, Issue, Registry) 41 | 42 | const RegistryWithData = () => { 43 | const { status } = useSecureLookup(OAUTH2_CLIENT) 44 | const [registry, updateRegistry] = useState(status) 45 | const [registrar, showRegistrar] = useState(false) 46 | 47 | useEffect(() => { 48 | updateRegistry(status) 49 | }, [status]) 50 | 51 | const revoke = id => { 52 | updateRegistry(new SUCCESS(registry.content.filter(x => x.access !== id))) 53 | } 54 | 55 | const append = app => { 56 | updateRegistry(new SUCCESS(registry.content.concat([app]))) 57 | } 58 | 59 | return ( 60 | 61 | 62 | 63 | 64 | 65 | ) 66 | } 67 | 68 | export default RegistryWithData 69 | -------------------------------------------------------------------------------- /js/account/src/components/Registry/RegistryList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Code, 4 | AnchorButton, 5 | Intent, 6 | Icon, 7 | } from '@blueprintjs/core' 8 | import { 9 | useSecureRemove, 10 | PENDING, 11 | SUCCESS, 12 | FAILURE, 13 | } from 'react-hook-oauth2' 14 | 15 | const OAUTH2_CLIENT = process.env.REACT_APP_OAUTH2_CLIENT 16 | 17 | const Item = ({ client, revoke }) => { 18 | const { status, retry, sequence } = useSecureRemove(undefined) 19 | 20 | if (status instanceof SUCCESS) { 21 | revoke(client.access) 22 | } 23 | 24 | return (status.status instanceof SUCCESS 25 | ? null 26 | : ( 27 | 28 | {client.app} 29 | {client.access} 30 | {client.security} 31 | {client.redirect_uri} 32 | 33 | (status instanceof FAILURE 38 | ? retry() 39 | : sequence(`${OAUTH2_CLIENT}/${client.access}`))} 40 | > 41 | revoke 42 | 43 | 44 | 45 | {status instanceof FAILURE 46 | ? 47 | : } 48 | 49 | 50 | ) 51 | ) 52 | } 53 | 54 | 55 | const RegistryList = ({ content, revoke }) => ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {content.map(x => )} 69 | 70 |
ApplicationClient IDSecurityRedirect Uri  
71 | ) 72 | 73 | export default RegistryList 74 | -------------------------------------------------------------------------------- /js/account/src/components/Registry/RegistryNone.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { H5, Button, Intent } from '@blueprintjs/core' 3 | 4 | const RegistryNone = ({ showRegistrar }) => ( 5 |
6 |
No OAuth applications in your account...
7 |

OAuth applications are used to access REST API.

8 | 11 |
12 | ) 13 | 14 | export default RegistryNone 15 | -------------------------------------------------------------------------------- /js/account/src/components/Registry/index.js: -------------------------------------------------------------------------------- 1 | import Registry from './Registry' 2 | 3 | export default Registry 4 | -------------------------------------------------------------------------------- /js/account/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './app' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /js/signin/.env: -------------------------------------------------------------------------------- 1 | ## 2 | ## defines prefix for sign-in/sign-up urls 3 | ## REACT_APP_PREFIX=https://auth.example.com 4 | -------------------------------------------------------------------------------- /js/signin/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true, 6 | "browser": true 7 | }, 8 | "settings": { 9 | "import/resolver": { 10 | "node": { 11 | "paths": ["./src", "./src/style"], 12 | "extensions": [".js", ".jsx"] 13 | } 14 | } 15 | }, 16 | "extends": [ 17 | "eslint:recommended", 18 | "plugin:react/recommended", 19 | "airbnb" 20 | ], 21 | "parserOptions": { 22 | "ecmaVersion": 2018 23 | }, 24 | "rules": { 25 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 26 | "no-console": "error", 27 | "semi": ["off", "always"], 28 | "react/prop-types": "off" 29 | } 30 | } -------------------------------------------------------------------------------- /js/signin/Makefile: -------------------------------------------------------------------------------- 1 | ## @doc 2 | ## static site deployment with AWS Gateway 3 | .PHONY: all deps test clean dist dist-up dist-rm 4 | 5 | ## 6 | ## Global config 7 | APP ?= signin 8 | VSN ?= $(shell test -z "`git status --porcelain`" && git describe --tags --long | sed -e 's/-g[0-9a-f]*//' | sed -e 's/-0//' || echo "`git describe --abbrev=0 --tags`-a") 9 | SITE ?= ${VSN}.auth.${CONFIG_DOMAIN} 10 | ROOT ?= /oauth2/authorize 11 | BUILD = build 12 | 13 | ## 14 | ## 15 | all: test 16 | 17 | deps: | node_modules 18 | 19 | test: deps 20 | npm run lint 21 | 22 | .env.production: 23 | @echo "# do nothing" | tee -a $@ 24 | 25 | node_modules: 26 | @npm install 27 | 28 | clean: 29 | -@rm .env.production 30 | -@rm -Rf build 31 | 32 | ## 33 | ## 34 | dist: deps .env.production 35 | @PUBLIC_URL=${ROOT} npm run build 36 | 37 | dist-up: 38 | @aws s3 sync ${BUILD} s3://${SITE}/${APP}/ --delete 39 | @find ${BUILD} -name "*.html" | cut -d'/' -f2- |\ 40 | xargs -L 1 -I {} aws s3 cp ${BUILD}/{} s3://${SITE}/${APP}/{} --cache-control "max-age=20" 41 | 42 | dist-rm: 43 | @aws s3 rm s3://${SITE}/${APP} --recursive 44 | -------------------------------------------------------------------------------- /js/signin/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src" 4 | }, 5 | "exclude": ["node_modules", "build", "coverage", "dist", "lib"] 6 | } -------------------------------------------------------------------------------- /js/signin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2-default-ux", 3 | "version": "0.2.0", 4 | "private": true, 5 | "dependencies": { 6 | "@blueprintjs/core": "^3.19.1", 7 | "emotion-theming": "^10.0.19", 8 | "font-awesome": "^4.7.0", 9 | "query-string": "^6.9.0", 10 | "react": "^16.12.0", 11 | "react-dom": "^16.12.0", 12 | "react-router-dom": "^5.1.2", 13 | "reflexbox": "^4.0.6" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^6.7.1", 17 | "eslint-config-airbnb": "^18.0.1", 18 | "eslint-plugin-react": "^7.17.0", 19 | "react-scripts": "3.2.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test --env=jsdom", 25 | "eject": "react-scripts eject", 26 | "lint": "eslint src" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /js/signin/public/env.js: -------------------------------------------------------------------------------- 1 | // This file will not end up inside the main application JavaScript bundle. 2 | // Instead, it will simply be copied inside the build folder. 3 | // The generated "index.html" will require it just before this main bundle. 4 | // You can thus use it to define some environment variables that will 5 | // be made available synchronously in all your JS modules under "src". 6 | // 7 | // The content of this file is dynamically modified by REST API daemon 8 | // So that behavior of SignIn / SignUp process is customized without 9 | // needs to rebuild application (See restd_static:react_env_js/2) 10 | // 11 | // set feature value to false to disable it 12 | window.env = { 13 | // 14 | // Enables SignIn/SignUp using access/secret key pair 15 | KEYPAIR: true, 16 | 17 | // 18 | // Enable user initiated reset of secret key 19 | KEYPAIR_RESET: true, 20 | 21 | // 22 | // Enable SignIn using GitHub account 23 | GITHUB: "https://github.com/login/oauth/authorize?client_id=xxx&scope=xxx" 24 | } 25 | -------------------------------------------------------------------------------- /js/signin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogfish/oauth2/f758bf689833aa97947e7740e64cbb9211dfa5c2/js/signin/public/favicon.ico -------------------------------------------------------------------------------- /js/signin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sign-In 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /js/signin/src/app.css: -------------------------------------------------------------------------------- 1 | @import "../node_modules/@blueprintjs/core/lib/css/blueprint.css"; 2 | @import "../node_modules/font-awesome/css/font-awesome.min.css"; 3 | 4 | body 5 | { 6 | background: #F5F8FA; 7 | margin: 0; 8 | } 9 | 10 | .bp3-text-muted 11 | { 12 | font-size: 12px; 13 | color: #BFCCD6; 14 | } 15 | 16 | h1.bp3-heading 17 | { 18 | font-weight: 100; 19 | color: #293742; 20 | } 21 | 22 | input 23 | { 24 | width: 100%; 25 | } 26 | 27 | .oauth2-chip 28 | { 29 | display: inline-block; 30 | padding: 0 25px; 31 | height: 32px; 32 | line-height: 30px; 33 | border-radius: 16px; 34 | background-color: #f7f7f7; 35 | color: #4a4a4a; 36 | } 37 | 38 | .oauth2__secret, 39 | .oauth2__access 40 | { 41 | word-wrap: break-word; 42 | } 43 | -------------------------------------------------------------------------------- /js/signin/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | HashRouter as Router, 4 | Route, 5 | } from 'react-router-dom' 6 | import queryString from 'query-string' 7 | import { ThemeProvider } from 'emotion-theming' 8 | import SignIn from 'components/SignIn' 9 | import SignUp from 'components/SignUp' 10 | import SecretReset from 'components/SecretReset' 11 | import SecretRecover from 'components/SecretRecover' 12 | import './app.css' 13 | 14 | const params = queryString.parse(window.location.search) 15 | const oauth2 = { 16 | ...params, 17 | responseType: params.response_type || 'code', 18 | clientId: params.client_id || 'account@oauth2', 19 | state: params.state || '', 20 | redirectUri: params.redirect_uri || '', 21 | } 22 | 23 | const theme = { 24 | breakpoints: ['480px', '768px', '1024px'], 25 | space: [0, 4, 8, 16, 32, 64, 128, 256, 512], 26 | } 27 | 28 | const App = () => ( 29 | 30 | 31 | <> 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | 37 | 38 | 39 | ) 40 | 41 | export default App 42 | -------------------------------------------------------------------------------- /js/signin/src/components/AccessKey.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Label, Classes } from '@blueprintjs/core' 3 | 4 | const AccessKey = () => ( 5 | 16 | ) 17 | 18 | export default AccessKey 19 | -------------------------------------------------------------------------------- /js/signin/src/components/Dialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Flex, Box } from 'reflexbox' 3 | import { Card, Elevation, Divider } from '@blueprintjs/core' 4 | 5 | const Head = ({ icon, title }) => ( 6 |

7 |