├── .github └── workflows │ ├── build_and_deploy_master.yml │ └── build_branches_and_pull_requests.yml ├── .gitignore ├── LICENSE ├── README.md ├── gwt-oauth2 ├── pom.xml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── freddyboucher │ │ │ └── gwt │ │ │ └── oauth2 │ │ │ └── client │ │ │ ├── Auth.java │ │ │ ├── AuthImpl.java │ │ │ ├── AuthRequest.java │ │ │ ├── CookieStoreImpl.java │ │ │ ├── TokenStore.java │ │ │ └── TokenStoreImpl.java │ └── resources │ │ └── io │ │ └── github │ │ └── freddyboucher │ │ └── gwt │ │ └── oauth2 │ │ ├── OAuth2.gwt.xml │ │ └── resources │ │ └── oauthWindow.html │ └── test │ └── java │ └── io │ └── github │ └── freddyboucher │ └── gwt │ └── oauth2 │ ├── JUnitTests.java │ └── client │ ├── AuthRequestTest.java │ └── AuthTest.java ├── pom.xml └── sample ├── README.md ├── pom.xml ├── sample-client ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── github │ │ └── freddyboucher │ │ └── gwt │ │ └── oauth2 │ │ └── sample │ │ └── App.java │ └── module.gwt.xml ├── sample-server ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── github │ │ └── freddyboucher │ │ └── gwt │ │ └── oauth2 │ │ └── sample │ │ └── Oauth2ServiceImpl.java │ └── webapp │ ├── WEB-INF │ ├── appengine-web.xml │ ├── logging.properties │ └── web.xml │ ├── app │ └── oauthWindow.jsp │ ├── favicon.ico │ ├── index.html │ └── sample.css └── sample-shared ├── pom.xml └── src └── main └── java └── io └── github └── freddyboucher └── gwt └── oauth2 └── sample ├── Oauth2Service.java └── Oauth2ServiceAsync.java /.github/workflows/build_and_deploy_master.yml: -------------------------------------------------------------------------------- 1 | name: Build & Deploy master 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | mavenBuild: 9 | name: Maven build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Set up JDK 1.8 14 | uses: actions/setup-java@master 15 | with: 16 | java-version: 8 17 | distribution: 'adopt' 18 | check-latest: true 19 | - name: Build with Maven 20 | run: mvn clean install -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn 21 | - uses: google-github-actions/setup-gcloud@master 22 | with: 23 | service_account_key: ${{ secrets.GCLOUD_AUTH }} 24 | export_default_credentials: true 25 | - run: gcloud --quiet --verbosity=warning --project=gwt-oauth2 app deploy sample/sample-server/target/sample-server-1.3-SNAPSHOT --promote --version=snapshot 26 | -------------------------------------------------------------------------------- /.github/workflows/build_branches_and_pull_requests.yml: -------------------------------------------------------------------------------- 1 | name: Build branches and pull requests 2 | on: 3 | pull_request: 4 | push: 5 | branches-ignore: 6 | - master 7 | 8 | jobs: 9 | mavenBuild: 10 | name: Maven build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Set up JDK 1.8 15 | uses: actions/setup-java@master 16 | with: 17 | java-version: 8 18 | distribution: 'adopt' 19 | check-latest: true 20 | - name: Build with Maven 21 | run: mvn clean install -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | /bin 3 | *.class 4 | *.jar 5 | *.war 6 | .apt_generated/ 7 | .project 8 | .metadata 9 | bin/** 10 | tmp/** 11 | tmp/**/* 12 | *.tmp 13 | *.bak 14 | *.swp 15 | *~.nib 16 | local.properties 17 | .classpath 18 | .settings/ 19 | .loadpath 20 | .factorypath 21 | .DS_Store 22 | *.iml 23 | *.idea 24 | .idea/ 25 | pom.xml.releaseBackup 26 | release.properties -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gwt-oauth2 2 | ========== 3 | 4 | * [gwt-oauth2](gwt-oauth2) : The core library 5 | * [sample](sample) : A demo application 6 | 7 | Live demo: [https://gwt-oauth2.appspot.com/](https://gwt-oauth2.appspot.com/) 8 | 9 | Quick start 10 | ------------- 11 | 12 | 1\. Add the Maven dependency 13 | ```xml 14 | 15 | io.github.freddyboucher 16 | gwt-oauth2 17 | 1.2 18 | provided 19 | 20 | ``` 21 | 22 | 2\. Add `` to your GWT module XML file. 23 | 24 | 3\. Use it as follow: 25 | ```java 26 | public class App implements EntryPoint { 27 | @Override 28 | public void onModuleLoad() { 29 | Button button = new Button("Google", (ClickHandler) event -> { 30 | AuthRequest req = 31 | new AuthRequest("https", "accounts.google.com", "o/oauth2/auth", "GOOGLE_CLIENT_ID") 32 | .setParameter("scope", "email profile openid"); 33 | Auth.get().login(req, new Callback, Throwable>() { 34 | @Override 35 | public void onFailure(Throwable reason) { 36 | GWT.log(null, reason); 37 | } 38 | 39 | @Override 40 | public void onSuccess(Map result) { 41 | String token = result.get("access_token"); 42 | GWT.log(token); 43 | } 44 | }, "access_token"); 45 | }); 46 | RootPanel.get().add(button); 47 | } 48 | } 49 | ``` 50 | 51 | Release Notes 52 | ------------- 53 | 54 | - 1.2 55 | - Fix: Deal with issue that oauth window cannot access parent window custom JS by switching to 56 | window.postMessage (as a result, you do not need to add the `generateJsInteropExports` 57 | compilation option to your project anymore). 58 | - 1.1 59 | - Fix: Avoid Google api.js name collision by renaming `oauth2` to `gwtOAuth2` 60 | - 1.0 61 | - Expose TokenStore and add TokenStore#keySet and TokenStore#remove 62 | - 1.0-RC2 63 | - Fix: Uncaught TypeError: Cannot set property 'innerText' of null 64 | - Add 'viewport' meta to have a readable message on Mobile device 65 | - 1.0-RC1 66 | - Update GWT to 2.9.0 67 | - Migrate JSNI to JsInterop / elemental2 68 | - 0.5 69 | - Map result - values are still URL encoded #10 70 | - 0.4 71 | - Map result - values are URL encoded #8 72 | - Callback is never consumed when manually closing the login window #9 73 | - 0.3 74 | - Initial release 75 | 76 | Contributing 77 | ------------- 78 | 79 | Code must pass [Google Java Style Guide](https://checkstyle.sourceforge.io/styleguides/google-java-style-20180523/javaguide.html). 80 | -------------------------------------------------------------------------------- /gwt-oauth2/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | 9 | Freddy Boucher 10 | freddy.boucher@gmail.com 11 | 12 | 13 | https://github.com/freddyboucher/gwt-oauth2 14 | 15 | io.github.freddyboucher 16 | gwt-oauth2 17 | 1.3-SNAPSHOT 18 | gwt-lib 19 | 20 | gwt-oauth2 21 | OAuth 2.0 Library for GWT 22 | 23 | 24 | Apache License, Version 2.0 25 | https://www.apache.org/licenses/LICENSE-2.0.txt 26 | repo 27 | A business-friendly OSS license 28 | 29 | 30 | 31 | 32 | 1.8 33 | 1.8 34 | 35 | UTF-8 36 | UTF-8 37 | 38 | 39 | 40 | 41 | com.google.gwt 42 | gwt-user 43 | 2.9.0 44 | provided 45 | 46 | 47 | com.google.elemental2 48 | elemental2-core 49 | 1.1.0 50 | 51 | 52 | com.google.elemental2 53 | elemental2-dom 54 | 1.1.0 55 | 56 | 57 | 58 | 59 | com.google.gwt 60 | gwt-dev 61 | 2.9.0 62 | test 63 | 64 | 65 | junit 66 | junit 67 | 4.13.2 68 | test 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-checkstyle-plugin 77 | 3.1.1 78 | 79 | google_checks.xml 80 | UTF-8 81 | true 82 | true 83 | false 84 | 0 85 | 86 | 87 | 88 | validate 89 | 90 | check 91 | 92 | 93 | 94 | 95 | 96 | com.puppycrawl.tools 97 | checkstyle 98 | 8.33 99 | 100 | 101 | 102 | 103 | net.ltgt.gwt.maven 104 | gwt-maven-plugin 105 | 1.0.0 106 | true 107 | 108 | auto 109 | io.github.freddyboucher.gwt.oauth2.OAuth2 110 | src/main/resources/io/github/freddyboucher/gwt/oauth2/OAuth2.gwt.xml 111 | 112 | 113 | **/JUnitTests.java 114 | 115 | 116 | 117 | 118 | maven-deploy-plugin 119 | 2.8.2 120 | 121 | 122 | default-deploy 123 | deploy 124 | 125 | deploy 126 | 127 | 128 | 129 | 130 | 131 | org.sonatype.plugins 132 | nexus-staging-maven-plugin 133 | 1.6.8 134 | true 135 | 136 | ossrh 137 | https://oss.sonatype.org/ 138 | true 139 | 140 | 141 | 142 | org.apache.maven.plugins 143 | maven-source-plugin 144 | 3.2.1 145 | 146 | 147 | attach-sources 148 | 149 | jar 150 | 151 | 152 | 153 | 154 | 155 | org.apache.maven.plugins 156 | maven-javadoc-plugin 157 | 3.1.1 158 | 159 | UTF-8 160 | 161 | 162 | 163 | attach-javadoc 164 | 165 | jar 166 | 167 | 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-release-plugin 173 | 2.5.3 174 | 175 | true 176 | false 177 | forked-path 178 | -Dgpg.passphrase=${gpg.passphrase} 179 | 180 | 181 | 182 | org.apache.maven.scm 183 | maven-scm-provider-gitexe 184 | 1.11.2 185 | 186 | 187 | org.apache.maven.scm 188 | maven-scm-api 189 | 1.11.2 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | ossrh 199 | https://oss.sonatype.org/content/repositories/snapshots 200 | 201 | 202 | ossrh 203 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 204 | 205 | 206 | 207 | 208 | 209 | scm:git:git://github.com/dexecutor/dependent-tasks-executor.git 210 | scm:git:git@github.com:dexecutor/dexecutor.git 211 | https://github.com/dexecutor/dependent-tasks-executor 212 | 1.3-SNAPSHOT 213 | 214 | 215 | 216 | 217 | 218 | release-sign-artifacts 219 | 220 | 221 | performRelease 222 | true 223 | 224 | 225 | 226 | 227 | 228 | org.apache.maven.plugins 229 | maven-gpg-plugin 230 | 1.6 231 | 232 | 233 | sign-artifacts 234 | verify 235 | 236 | sign 237 | 238 | 239 | 240 | --pinentry-mode 241 | loopback 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /gwt-oauth2/src/main/java/io/github/freddyboucher/gwt/oauth2/client/Auth.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.github.freddyboucher.gwt.oauth2.client; 18 | 19 | import com.google.gwt.core.client.Callback; 20 | import com.google.gwt.core.client.Scheduler; 21 | import com.google.gwt.http.client.URL; 22 | import com.google.gwt.json.client.JSONNumber; 23 | import com.google.gwt.json.client.JSONObject; 24 | import com.google.gwt.json.client.JSONParser; 25 | import com.google.gwt.json.client.JSONString; 26 | import java.util.Arrays; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | /** 32 | * Provides methods to manage authentication flow. 33 | * 34 | * @author jasonhall@google.com (Jason Hall) 35 | */ 36 | public abstract class Auth { 37 | 38 | /** 39 | * Instance of the {Auth} to use in a GWT application. 40 | */ 41 | public static Auth get() { 42 | return AuthImpl.INSTANCE; 43 | } 44 | 45 | protected final TokenStore tokenStore; 46 | private final Clock clock; 47 | protected final Scheduler scheduler; 48 | 49 | protected int height = 600; 50 | protected int width = 800; 51 | 52 | Auth(TokenStore tokenStore, Clock clock, Scheduler scheduler) { 53 | this.tokenStore = tokenStore; 54 | this.clock = clock; 55 | this.scheduler = scheduler; 56 | } 57 | 58 | private AuthRequest lastRequest; 59 | private List lastRequiredParams; 60 | private Callback, Throwable> lastCallback; 61 | 62 | private static final double TEN_MINUTES = 10 * 60 * 1000; 63 | 64 | /** 65 | * Request an access token from an OAuth 2.0 provider. 66 | * 67 | *

If it can be determined that the user has already granted access, and the token has not yet 68 | * expired, and that the token will not expire soon, the existing token will be passed to the 69 | * callback. 70 | * 71 | *

Otherwise, a popup window will be displayed which may prompt the user to grant access. If 72 | * the user has already granted access the popup will immediately close and the token will be 73 | * passed to the callback. If access hasn't been granted, the user will be prompted, and when they 74 | * grant, the token will be passed to the callback. 75 | * 76 | * @param req Request for authentication. 77 | * @param callback Callback to pass the token to when access has been granted. 78 | * @param requiredParams The required params. It calls the callback#onFailure if response doesn't 79 | * contain all required params. 80 | */ 81 | public void login(AuthRequest req, Callback, Throwable> callback, 82 | String... requiredParams) { 83 | lastRequest = req; 84 | lastCallback = new Callback, Throwable>() { 85 | private boolean consumed; 86 | 87 | @Override 88 | public void onFailure(Throwable reason) { 89 | if (!consumed) { 90 | consumed = true; 91 | callback.onFailure(reason); 92 | } 93 | } 94 | 95 | @Override 96 | public void onSuccess(Map result) { 97 | if (!consumed) { 98 | consumed = true; 99 | callback.onSuccess(result); 100 | } 101 | } 102 | }; 103 | lastRequiredParams = Arrays.asList(requiredParams); 104 | 105 | String authUrl = req.buildString(); 106 | 107 | // Try to look up the token we have stored. 108 | TokenInfo info = getToken(req); 109 | if (null == info || null == info.expires || expiringSoon(info)) { 110 | // Token wasn't found, or doesn't have an expiration, or is expired or 111 | // expiring soon. Requesting access will refresh the token. 112 | doLogin(authUrl, lastCallback); 113 | } else { 114 | // Token was found and is good, immediately execute the callback with the 115 | // access token. 116 | 117 | scheduler.scheduleDeferred(() -> answerCallback(info)); 118 | } 119 | } 120 | 121 | private void answerCallback(TokenInfo info) { 122 | if (info.params.keySet().containsAll(lastRequiredParams)) { 123 | lastCallback.onSuccess(info.params); 124 | } else { 125 | lastCallback.onFailure( 126 | new RuntimeException( 127 | "Could not find required params: " 128 | + lastRequiredParams 129 | + " in response: " 130 | + info.params)); 131 | } 132 | } 133 | 134 | /** 135 | * Returns whether or not the token will be expiring within the next ten minutes. 136 | */ 137 | boolean expiringSoon(TokenInfo info) { 138 | // TODO(jasonhall): Consider varying the definition of "soon" based on the 139 | // original expires_in value (e.g., "soon" = 1/10th of the total time before 140 | // it's expired). 141 | return info.expires < clock.now() + TEN_MINUTES; 142 | } 143 | 144 | /** 145 | * Get the OAuth 2.0 token for which this application may not have already been granted access, by 146 | * displaying a popup to the user. 147 | */ 148 | abstract void doLogin(String authUrl, Callback, Throwable> callback); 149 | 150 | /** 151 | * Sets the height of the OAuth 2.0 popup dialog, in pixels. The default is 600px. 152 | */ 153 | public Auth setWindowHeight(int height) { 154 | this.height = height; 155 | return this; 156 | } 157 | 158 | /* Sets the width of the OAuth 2.0 popup dialog, in pixels. The default is 800px. */ 159 | public Auth setWindowWidth(int width) { 160 | this.width = width; 161 | return this; 162 | } 163 | 164 | /** 165 | * Called by the {@code doLogin()} method which is registered as a global variable on the page. 166 | */ 167 | // This method is called via a global method defined in AuthImpl.register() 168 | @SuppressWarnings("unused") 169 | void finish(String response) { 170 | if (response.startsWith("#") || response.startsWith("?")) { 171 | Map params = new HashMap<>(); 172 | 173 | // Iterate over keys and values in the string hash value to find relevant 174 | // information like the access token or an error message. The string will be 175 | // in the form of: #key1=val1&key2=val2&key3=val3 (etc.) 176 | int idx = 1; 177 | while (idx < response.length() - 1) { 178 | // Grab the next key (between start and '=') 179 | int nextEq = response.indexOf('=', idx); 180 | if (0 > nextEq) { 181 | break; 182 | } 183 | String key = response.substring(idx, nextEq); 184 | 185 | // Grab the next value (between '=' and '&') 186 | int nextAmp = response.indexOf('&', nextEq); 187 | nextAmp = nextAmp < 0 ? response.length() : nextAmp; 188 | String val = response.substring(nextEq + 1, nextAmp); 189 | 190 | // Start looking from here from now on. 191 | idx = nextAmp + 1; 192 | 193 | params.put(key, URL.decodeQueryString(val)); 194 | } 195 | 196 | if (params.containsKey("error")) { 197 | StringBuilder builder = 198 | new StringBuilder("Error from provider: ").append(params.get("error")); 199 | if (params.containsKey("error_description")) { 200 | builder.append(" (").append(params.get("error_description")).append(")"); 201 | } 202 | if (params.containsKey("error_uri")) { 203 | builder.append("; see: ").append(params.get("error_uri")); 204 | } 205 | lastCallback.onFailure(new RuntimeException(builder.toString())); 206 | } else { 207 | Double expires; 208 | if (params.containsKey("expires_in")) { 209 | // expires_in is seconds, convert to milliseconds and add to now 210 | double expiresIn = Double.parseDouble(params.get("expires_in")) * 1000; 211 | expires = clock.now() + expiresIn; 212 | } else { 213 | expires = null; 214 | } 215 | TokenInfo info = new TokenInfo(expires, params); 216 | setToken(lastRequest, info); 217 | answerCallback(info); 218 | } 219 | } else { 220 | lastCallback.onFailure(new RuntimeException("Invalid hash: " + response)); 221 | } 222 | } 223 | 224 | /** 225 | * Test-compatible abstraction for getting the current time. 226 | */ 227 | interface Clock { 228 | 229 | // Using double to avoid longs in GWT, which are slow. 230 | double now(); 231 | } 232 | 233 | TokenInfo getToken(AuthRequest req) { 234 | String tokenStr = tokenStore.get(req.buildString()); 235 | if (null != tokenStr) { 236 | try { 237 | return TokenInfo.fromJson(tokenStr); 238 | } catch (Exception e) { 239 | tokenStore.remove(req.buildString()); 240 | } 241 | } 242 | return null; 243 | } 244 | 245 | void setToken(AuthRequest req, TokenInfo info) { 246 | tokenStore.set(req.buildString(), info.asJson()); 247 | } 248 | 249 | /** 250 | * Clears all tokens stored by this class. 251 | * 252 | *

This will result in subsequent calls to {@link #login(AuthRequest, Callback, String...)} 253 | * displaying a popup to the user. If the user has already granted access, that popup will 254 | * immediately close. 255 | */ 256 | public void clearAllTokens() { 257 | tokenStore.clear(); 258 | } 259 | 260 | public TokenStore getTokenStore() { 261 | return tokenStore; 262 | } 263 | 264 | /** 265 | * Encapsulates information an access token and when it will expire. 266 | */ 267 | protected static class TokenInfo { 268 | 269 | protected final Map params; 270 | protected final Double expires; 271 | 272 | public TokenInfo(Double expires, Map params) { 273 | this.expires = expires; 274 | this.params = params; 275 | } 276 | 277 | public String asJson() { 278 | JSONObject root = new JSONObject(); 279 | if (null != expires) { 280 | root.put("expires", new JSONNumber(expires)); 281 | } 282 | JSONObject paramsJsonObject = new JSONObject(); 283 | for (Map.Entry entry : params.entrySet()) { 284 | paramsJsonObject.put(entry.getKey(), new JSONString(entry.getValue())); 285 | } 286 | root.put("params", paramsJsonObject); 287 | return root.toString(); 288 | } 289 | 290 | public static TokenInfo fromJson(String val) { 291 | Map response = new HashMap<>(); 292 | JSONObject root = JSONParser.parseStrict(val).isObject(); 293 | Double expires; 294 | if (root.containsKey("expires")) { 295 | expires = root.get("expires").isNumber().doubleValue(); 296 | } else { 297 | expires = null; 298 | } 299 | JSONObject responseJsonObject = root.get("params").isObject(); 300 | for (String key : responseJsonObject.keySet()) { 301 | response.put(key, responseJsonObject.get(key).isString().stringValue()); 302 | } 303 | return new TokenInfo(expires, response); 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /gwt-oauth2/src/main/java/io/github/freddyboucher/gwt/oauth2/client/AuthImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.github.freddyboucher.gwt.oauth2.client; 18 | 19 | import com.google.gwt.core.client.Callback; 20 | import com.google.gwt.core.client.Duration; 21 | import com.google.gwt.core.client.Scheduler; 22 | import com.google.gwt.core.client.Scheduler.RepeatingCommand; 23 | import com.google.gwt.storage.client.Storage; 24 | import elemental2.dom.DomGlobal; 25 | import elemental2.dom.Event; 26 | import elemental2.dom.EventListener; 27 | import elemental2.dom.MessageEvent; 28 | import elemental2.dom.Window; 29 | import java.util.Map; 30 | 31 | /** 32 | * Real implementation of {@link Auth}, used in real GWT applications. 33 | * 34 | * @author jasonhall@google.com (Jason Hall) 35 | */ 36 | class AuthImpl extends Auth { 37 | 38 | static final AuthImpl INSTANCE = new AuthImpl(); 39 | 40 | private Window window; 41 | 42 | AuthImpl() { 43 | super(Storage.isLocalStorageSupported() ? new TokenStoreImpl() : new CookieStoreImpl(), 44 | () -> Duration.currentTimeMillis(), Scheduler.get()); 45 | } 46 | 47 | /** 48 | * Get the OAuth 2.0 token for which this application may not have already been granted access, by 49 | * displaying a popup to the user. 50 | */ 51 | @Override 52 | void doLogin(String authUrl, Callback, Throwable> callback) { 53 | DomGlobal.window.addEventListener("message", new EventListener() { 54 | @Override 55 | public void handleEvent(Event evt) { 56 | if (evt instanceof MessageEvent) { 57 | MessageEvent messageEvent = (MessageEvent) evt; 58 | if (DomGlobal.location.origin.equalsIgnoreCase(messageEvent.origin)) { 59 | finish(String.valueOf(messageEvent.data)); 60 | DomGlobal.window.removeEventListener("message", this); 61 | } 62 | } 63 | } 64 | }); 65 | if (null != window && !window.closed) { 66 | callback.onFailure(new IllegalStateException("Authentication in progress")); 67 | } else { 68 | window = DomGlobal.window 69 | .open(authUrl, "popupWindow", "width=" + width + ",height=" + height); 70 | if (null == window) { 71 | callback.onFailure( 72 | new RuntimeException("The authentication popup window appears to have been blocked")); 73 | } else { 74 | scheduler.scheduleEntry(new RepeatingCommand() { 75 | @Override 76 | public boolean execute() { 77 | if (window.closed) { 78 | callback.onFailure(new RuntimeException( 79 | "The authentication popup window appears to have been closed")); 80 | } 81 | return !window.closed; 82 | } 83 | }); 84 | } 85 | } 86 | } 87 | 88 | @Override 89 | void finish(String response) { 90 | // Clean up the popup 91 | if (null != window && !window.closed) { 92 | window.close(); 93 | } 94 | super.finish(response); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /gwt-oauth2/src/main/java/io/github/freddyboucher/gwt/oauth2/client/AuthRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.github.freddyboucher.gwt.oauth2.client; 18 | 19 | import com.google.gwt.core.client.GWT; 20 | import com.google.gwt.http.client.UrlBuilder; 21 | 22 | /** 23 | * Represents a request for authentication to an OAuth 2.0 provider server. 24 | * 25 | * @author jasonhall@google.com (Jason Hall) 26 | */ 27 | public class AuthRequest extends UrlBuilder { 28 | 29 | public AuthRequest() { 30 | } 31 | 32 | /** 33 | * * Construct an AuthRequest with response_type=token. 34 | * 35 | * @param protocol The protocol: http / https 36 | * @param host The host 37 | * @param path The port 38 | * @param clientId The client_id 39 | */ 40 | public AuthRequest(String protocol, String host, String path, String clientId) { 41 | this(protocol, host, null, path, clientId); 42 | } 43 | 44 | /** 45 | * Construct an AuthRequest with response_type=token. 46 | * 47 | * @param protocol The protocol: http / https 48 | * @param host The host 49 | * @param port The port 50 | * @param path The path 51 | * @param clientId The client_id 52 | */ 53 | public AuthRequest(String protocol, String host, Integer port, String path, String clientId) { 54 | if (null != protocol) { 55 | super.setProtocol(protocol); 56 | } 57 | super.setHost(host); 58 | if (null != port) { 59 | super.setPort(port); 60 | } 61 | super.setPath(path); 62 | super.setParameter("client_id", clientId); 63 | super.setParameter("response_type", "token"); 64 | super.setParameter("redirect_uri", GWT.getModuleBaseURL() + "oauthWindow.html"); 65 | } 66 | 67 | @Override 68 | public AuthRequest setParameter(String key, String... values) { 69 | super.setParameter(key, values); 70 | return this; 71 | } 72 | 73 | @Override 74 | public AuthRequest removeParameter(String name) { 75 | super.removeParameter(name); 76 | return this; 77 | } 78 | 79 | @Override 80 | public AuthRequest setHash(String hash) { 81 | super.setHash(hash); 82 | return this; 83 | } 84 | 85 | @Override 86 | public AuthRequest setHost(String host) { 87 | super.setHost(host); 88 | return this; 89 | } 90 | 91 | @Override 92 | public AuthRequest setPath(String path) { 93 | super.setPath(path); 94 | return this; 95 | } 96 | 97 | @Override 98 | public AuthRequest setPort(int port) { 99 | super.setPort(port); 100 | return this; 101 | } 102 | 103 | @Override 104 | public AuthRequest setProtocol(String protocol) { 105 | super.setProtocol(protocol); 106 | return this; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gwt-oauth2/src/main/java/io/github/freddyboucher/gwt/oauth2/client/CookieStoreImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.github.freddyboucher.gwt.oauth2.client; 18 | 19 | import com.google.gwt.user.client.Cookies; 20 | import java.util.Set; 21 | import java.util.stream.Collectors; 22 | 23 | class CookieStoreImpl implements TokenStore { 24 | 25 | @Override 26 | public void set(String key, String value) { 27 | Cookies.setCookie(STORAGE_PREFIX + key, value); 28 | } 29 | 30 | @Override 31 | public String get(String key) { 32 | return Cookies.getCookie(STORAGE_PREFIX + key); 33 | } 34 | 35 | @Override 36 | public void remove(String key) { 37 | Cookies.removeCookie(STORAGE_PREFIX + key); 38 | } 39 | 40 | @Override 41 | public Set keySet() { 42 | return Cookies.getCookieNames().stream().filter(key -> key.startsWith(STORAGE_PREFIX)) 43 | .map(key -> key.substring(STORAGE_PREFIX.length())).collect(Collectors.toSet()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gwt-oauth2/src/main/java/io/github/freddyboucher/gwt/oauth2/client/TokenStore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.github.freddyboucher.gwt.oauth2.client; 18 | 19 | import java.util.Set; 20 | 21 | /** 22 | * Interface for storing, retrieving, and clearing stored tokens. 23 | * 24 | * @author jasonhall@google.com (Jason Hall) 25 | */ 26 | public interface TokenStore { 27 | 28 | String STORAGE_PREFIX = "gwt-oauth2-"; 29 | 30 | void set(String key, String value); 31 | 32 | String get(String key); 33 | 34 | void remove(String key); 35 | 36 | Set keySet(); 37 | 38 | default void clear() { 39 | keySet().forEach(this::remove); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gwt-oauth2/src/main/java/io/github/freddyboucher/gwt/oauth2/client/TokenStoreImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.github.freddyboucher.gwt.oauth2.client; 18 | 19 | import com.google.gwt.storage.client.Storage; 20 | import java.util.Set; 21 | import java.util.stream.Collectors; 22 | import java.util.stream.IntStream; 23 | 24 | class TokenStoreImpl implements TokenStore { 25 | 26 | private final Storage storage; 27 | 28 | TokenStoreImpl() { 29 | storage = Storage.getLocalStorageIfSupported(); 30 | } 31 | 32 | @Override 33 | public void set(String key, String value) { 34 | storage.setItem(STORAGE_PREFIX + key, value); 35 | } 36 | 37 | @Override 38 | public String get(String key) { 39 | return storage.getItem(STORAGE_PREFIX + key); 40 | } 41 | 42 | @Override 43 | public void remove(String key) { 44 | storage.removeItem(STORAGE_PREFIX + key); 45 | } 46 | 47 | @Override 48 | public Set keySet() { 49 | return IntStream.range(0, storage.getLength()).mapToObj(storage::key) 50 | .filter(key -> key.startsWith(STORAGE_PREFIX)) 51 | .map(key -> key.substring(STORAGE_PREFIX.length())).collect(Collectors.toSet()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /gwt-oauth2/src/main/resources/io/github/freddyboucher/gwt/oauth2/OAuth2.gwt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /gwt-oauth2/src/main/resources/io/github/freddyboucher/gwt/oauth2/resources/oauthWindow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /gwt-oauth2/src/test/java/io/github/freddyboucher/gwt/oauth2/JUnitTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.github.freddyboucher.gwt.oauth2; 18 | 19 | import com.google.gwt.junit.tools.GWTTestSuite; 20 | import io.github.freddyboucher.gwt.oauth2.client.AuthRequestTest; 21 | import io.github.freddyboucher.gwt.oauth2.client.AuthTest; 22 | import junit.framework.Test; 23 | 24 | /** 25 | * Test Suite for the {@code gwt-oauth2} library. 26 | * 27 | * @author jasonhall@google.com (Jason Hall) 28 | */ 29 | public class JUnitTests { 30 | 31 | public static Test suite() { 32 | GWTTestSuite suite = new GWTTestSuite("JUnit tests for OAuth2 library"); 33 | suite.addTestSuite(AuthTest.class); 34 | suite.addTestSuite(AuthRequestTest.class); 35 | return suite; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /gwt-oauth2/src/test/java/io/github/freddyboucher/gwt/oauth2/client/AuthRequestTest.java: -------------------------------------------------------------------------------- 1 | package io.github.freddyboucher.gwt.oauth2.client; 2 | 3 | import com.google.gwt.junit.client.GWTTestCase; 4 | 5 | public class AuthRequestTest extends GWTTestCase { 6 | 7 | @Override 8 | public String getModuleName() { 9 | return "io.github.freddyboucher.gwt.oauth2.OAuth2"; 10 | } 11 | 12 | public void testBuildString() { 13 | AuthRequest req1 = new AuthRequest("http", "host", "path", "clientId") 14 | .setParameter("scope", "scope").setParameter("redirect_uri", "uri"); 15 | assertEquals( 16 | "http://host/path?client_id=clientId&response_type=token&redirect_uri=uri&scope=scope", 17 | req1.buildString()); 18 | 19 | AuthRequest req2 = new AuthRequest("http", "host", "path", "clientId") 20 | .setParameter("scope", "scope").setParameter("redirect_uri", "uri"); 21 | assertEquals( 22 | "http://host/path?client_id=clientId&response_type=token&redirect_uri=uri&scope=scope", 23 | req2.buildString()); 24 | 25 | AuthRequest req3 = 26 | new AuthRequest("http", "host", "path", "clientId").setParameter("scope", "scope") 27 | .setParameter("key", "value").setParameter("redirect_uri", "uri"); 28 | assertEquals( 29 | "http://host/path?client_id=clientId&response_type=token&redirect_uri=uri&scope=scope&key=value", 30 | req3.buildString()); 31 | 32 | AuthRequest req4 = new AuthRequest("http", "host", "path", "clientId") 33 | .setParameter("scope", "scope1 scope2").setParameter("redirect_uri", "uri"); 34 | assertEquals( 35 | "http://host/path?client_id=clientId&response_type=token&redirect_uri=uri&scope=scope1+scope2", 36 | req4.buildString()); 37 | 38 | AuthRequest req5 = new AuthRequest("http", "host", "path", "clientId") 39 | .setParameter("scope", "scope1,scope2").setParameter("redirect_uri", "uri"); 40 | assertEquals( 41 | "http://host/path?client_id=clientId&response_type=token&redirect_uri=uri&scope=scope1%2Cscope2", 42 | req5.buildString()); 43 | 44 | AuthRequest req6 = new AuthRequest("http", "host", "path", "clientId") 45 | .setParameter("scope", "scope1", "scope2").setParameter("redirect_uri", "uri"); 46 | assertEquals( 47 | "http://host/path?client_id=clientId&response_type=token&redirect_uri=uri&scope=scope1&scope=scope2", 48 | req6.buildString()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gwt-oauth2/src/test/java/io/github/freddyboucher/gwt/oauth2/client/AuthTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.github.freddyboucher.gwt.oauth2.client; 18 | 19 | import com.google.gwt.core.client.Callback; 20 | import com.google.gwt.core.client.Scheduler.ScheduledCommand; 21 | import com.google.gwt.core.client.testing.StubScheduler; 22 | import com.google.gwt.junit.client.GWTTestCase; 23 | import io.github.freddyboucher.gwt.oauth2.client.Auth.TokenInfo; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Set; 28 | 29 | /** 30 | * Tests for {@link Auth}. 31 | * 32 | * @author jasonhall@google.com (Jason Hall) 33 | */ 34 | public class AuthTest extends GWTTestCase { 35 | 36 | private MockAuth auth; 37 | 38 | @Override 39 | public String getModuleName() { 40 | return "io.github.freddyboucher.gwt.oauth2.OAuth2"; 41 | } 42 | 43 | @Override 44 | protected void gwtSetUp() throws Exception { 45 | super.gwtSetUp(); 46 | auth = new MockAuth(); 47 | } 48 | 49 | /** 50 | * When the request does not have a token stored, the popup is used to get the token. 51 | */ 52 | public void testLogin_noToken() { 53 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 54 | .setParameter("scope", "scope").setParameter("redirect_uri", "uri"); 55 | MockCallback callback = new MockCallback(); 56 | auth.login(req, callback, "access_token"); 57 | 58 | // The popup was used and the iframe wasn't. 59 | assertTrue(auth.loggedInViaPopup); 60 | assertEquals( 61 | "http://host/path?client_id=clientId&response_type=token&redirect_uri=uri&scope=scope", 62 | auth.lastUrl); 63 | } 64 | 65 | /** 66 | * When the token is found in cookies, but may expire soon, the popup will be used to refresh the 67 | * token. 68 | */ 69 | public void testLogin_expiringSoon() { 70 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 71 | .setParameter("scope", "scope").setParameter("redirect_uri", "uri"); 72 | 73 | // Storing a token that expires soon (in just under 10 minutes) 74 | TokenInfo info = new TokenInfo(MockClock.now + 10 * 60 * 1000 - 1, new HashMap<>()); 75 | auth.setToken(req, info); 76 | 77 | MockCallback callback = new MockCallback(); 78 | auth.login(req, callback, "access_token"); 79 | 80 | assertTrue(auth.expiringSoon(info)); 81 | 82 | assertTrue(auth.loggedInViaPopup); 83 | assertEquals( 84 | "http://host/path?client_id=clientId&response_type=token&redirect_uri=uri&scope=scope", 85 | auth.lastUrl); 86 | } 87 | 88 | /** 89 | * When the token is found in cookies and will not expire soon, neither popup nor iframe is used, 90 | * and the token is immediately passed to the callback. 91 | */ 92 | public void testLogin_notExpiringSoon() { 93 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 94 | .setParameter("scope", "scope"); 95 | 96 | // Storing a token that does not expire soon (in exactly 10 minutes) 97 | HashMap params = new HashMap<>(); 98 | params.put("access_token", "foo"); 99 | TokenInfo info = new TokenInfo(MockClock.now + 10 * 60 * 1000, params); 100 | auth.setToken(req, info); 101 | 102 | MockCallback callback = new MockCallback(); 103 | auth.login(req, callback, "access_token"); 104 | 105 | // A deferred command will have been scheduled. Execute it. 106 | List deferred = ((StubScheduler) auth.scheduler) 107 | .getScheduledCommands(); 108 | assertEquals(1, deferred.size()); 109 | deferred.get(0).execute(); 110 | 111 | // The iframe was used and the popup wasn't. 112 | assertFalse(auth.loggedInViaPopup); 113 | 114 | // onSuccess() was called and onFailure() wasn't. 115 | assertEquals(params, callback.params); 116 | assertNull(callback.failure); 117 | } 118 | 119 | /** 120 | * When the token is found in cookies and does not specify an expire time, the iframe will be used 121 | * to refresh the token without displaying the popup. 122 | */ 123 | public void testLogin_nullExpires() { 124 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 125 | .setParameter("scope", "scope"); 126 | 127 | // Storing a token with a null expires time 128 | TokenInfo info = new TokenInfo(null, new HashMap<>()); 129 | auth.setToken(req, info); 130 | 131 | MockCallback callback = new MockCallback(); 132 | auth.login(req, callback, "access_token"); 133 | 134 | // TODO(jasonhall): When Auth supports immediate mode for supporting 135 | // providers, a null expiration will trigger an iframe immediate-mode 136 | // refresh. Until then, the popup is always used. 137 | assertTrue(auth.loggedInViaPopup); 138 | } 139 | 140 | /** 141 | * When finish() is called, the callback passed to login() is executed with the correct token, and 142 | * a cookie is set with relevant information, expiring in the correct amount of time. 143 | */ 144 | public void testFinish() { 145 | // Reset the default value 146 | MockClock.now = 5000; 147 | 148 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 149 | .setParameter("scope", "scope"); 150 | MockCallback callback = new MockCallback(); 151 | auth.login(req, callback, "access_token"); 152 | 153 | for (String response : new String[]{"#access_token=foo&expires_in=10000", 154 | "?access_token=foo&expires_in=10000"}) { 155 | // Simulates the auth provider's response 156 | auth.finish(response); 157 | 158 | // onSuccess() was called and onFailure() wasn't 159 | assertEquals("{access_token=foo, expires_in=10000}", callback.params.toString()); 160 | assertNull(callback.failure); 161 | 162 | // A token was stored as a result 163 | InMemoryTokenStore ts = (InMemoryTokenStore) auth.tokenStore; 164 | assertEquals(1, ts.store.size()); 165 | 166 | // That token is clientId+scope -> foo+expires 167 | TokenInfo info = TokenInfo.fromJson(ts.store.get(req.buildString())); 168 | assertEquals("{access_token=foo, expires_in=10000}", info.params.toString()); 169 | assertEquals(10005000.0D, info.expires); 170 | } 171 | } 172 | 173 | public void testFinishDifferentResponseType() { 174 | // Reset the default value 175 | MockClock.now = 5000; 176 | 177 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 178 | .setParameter("scope", "scope").setParameter("response_type", "id_token"); 179 | MockCallback callback = new MockCallback(); 180 | auth.login(req, callback, "id_token"); 181 | 182 | // Simulates the auth provider's response 183 | auth.finish("#id_token=foo&expires_in=10000"); 184 | 185 | // onSuccess() was called and onFailure() wasn't 186 | assertEquals("{id_token=foo, expires_in=10000}", callback.params.toString()); 187 | assertNull(callback.failure); 188 | } 189 | 190 | /** 191 | * If finish() is passed an invalid hash from the auth provider, a RuntimeException will be passed 192 | * to the callback. 193 | */ 194 | public void testFinish_invalidHash() { 195 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 196 | .setParameter("scope", "scope"); 197 | MockCallback callback = new MockCallback(); 198 | auth.login(req, callback, "access_token"); 199 | 200 | // Simulates the auth provider's response 201 | auth.finish("foobarbaznonsense"); 202 | 203 | // onFailure() was called with a RuntimeException stating the error. 204 | assertNotNull(callback.failure); 205 | assertTrue(callback.failure instanceof RuntimeException); 206 | assertEquals("Invalid hash: foobarbaznonsense", callback.failure.getMessage()); 207 | 208 | // onSuccess() was not called. 209 | assertNull(callback.params); 210 | } 211 | 212 | public void testFinish_badHash() { 213 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 214 | .setParameter("scope", "scope"); 215 | MockCallback callback = new MockCallback(); 216 | auth.login(req, callback, "access_token"); 217 | 218 | // Simulates the auth provider's response 219 | auth.finish("#foobarbaznonsense"); 220 | 221 | // onFailure() was called with a RuntimeException stating the error. 222 | assertNotNull(callback.failure); 223 | assertTrue(callback.failure instanceof RuntimeException); 224 | assertEquals("Could not find required params: [access_token] in response: {}", 225 | callback.failure.getMessage()); 226 | 227 | // onSuccess() was not called. 228 | assertNull(callback.params); 229 | } 230 | 231 | public void testFinish_noRequiredParams() { 232 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 233 | .setParameter("scope", "scope"); 234 | MockCallback callback = new MockCallback(); 235 | auth.login(req, callback); 236 | 237 | // Simulates the auth provider's response 238 | auth.finish("#access_token=foo"); 239 | 240 | // onSuccess() was called and onFailure() wasn't 241 | assertEquals("{access_token=foo}", callback.params.toString()); 242 | assertNull(callback.failure); 243 | } 244 | 245 | public void testFinish_urlEncodedParams() { 246 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 247 | .setParameter("scope", "openid email"); 248 | MockCallback callback = new MockCallback(); 249 | auth.login(req, callback); 250 | 251 | // Simulates the auth provider's response 252 | auth.finish("#scope=openid+email"); 253 | 254 | // onSuccess() was called and onFailure() wasn't 255 | assertEquals("{scope=openid email}", callback.params.toString()); 256 | assertNull(callback.failure); 257 | } 258 | 259 | /** 260 | * If finish() is passed an access token but no expires time, a TokenInfo will be stored without 261 | * an expiration time. The next time auth is requested, the iframe will be used, see {@link 262 | * #testLogin_nullExpires()}. 263 | */ 264 | public void testFinish_noExpires() { 265 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 266 | .setParameter("scope", "scope"); 267 | MockCallback callback = new MockCallback(); 268 | auth.login(req, callback, "access_token"); 269 | 270 | // Simulates the auth provider's response 271 | auth.finish("#access_token=foo"); 272 | 273 | // onSuccess() was called and onFailure() wasn't 274 | assertEquals("{access_token=foo}", callback.params.toString()); 275 | assertNull(callback.failure); 276 | 277 | // A token was stored as a result 278 | InMemoryTokenStore ts = (InMemoryTokenStore) auth.tokenStore; 279 | assertEquals(1, ts.store.size()); 280 | 281 | // That token is clientId+scope -> foo+expires 282 | TokenInfo info = TokenInfo.fromJson(ts.store.get(req.buildString())); 283 | assertEquals("{access_token=foo}", info.params.toString()); 284 | assertNull(info.expires); 285 | } 286 | 287 | /** 288 | * If finish() is passed a hash that describes an error condition, a RuntimeException will be 289 | * passed to onFailure() with the provider's auth string. 290 | */ 291 | public void testFinish_error() { 292 | // Simulates the auth provider's error response, with the error first, last, 293 | // and in the middle of the hash, and as the only element in the hash. Also 294 | // finds error descriptions and error URIs. 295 | assertError("#error=redirect_uri_mismatch", 296 | "Error from provider: redirect_uri_mismatch"); 297 | assertError("#error=redirect_uri_mismatch&foo=bar", 298 | "Error from provider: redirect_uri_mismatch"); 299 | assertError("#foo=bar&error=redirect_uri_mismatch", 300 | "Error from provider: redirect_uri_mismatch"); 301 | assertError("#foo=bar&error=redirect_uri_mismatch&bar=baz", 302 | "Error from provider: redirect_uri_mismatch"); 303 | assertError( 304 | "#foo=bar&error=redirect_uri_mismatch&error_description=Bad%20dog%21", 305 | "Error from provider: redirect_uri_mismatch (Bad dog!)"); 306 | assertError( 307 | "#foo=bar&error=redirect_uri_mismatch&error_uri=example.com", 308 | "Error from provider: redirect_uri_mismatch; see: example.com"); 309 | assertError( 310 | "#foo=bar&error=redirect_uri_mismatch&error_description=Bad dog!&error_uri=example.com", 311 | "Error from provider: redirect_uri_mismatch (Bad dog!); see: example.com"); 312 | } 313 | 314 | private void assertError(String response, String error) { 315 | AuthRequest req = new AuthRequest("http", "host", "path", "clientId") 316 | .setParameter("scope", "scope"); 317 | MockCallback callback = new MockCallback(); 318 | auth.login(req, callback, "access_token"); 319 | // Simulates the auth provider's error response. 320 | auth.finish(response); 321 | 322 | // onFailure() was called with a RuntimeException stating the error. 323 | assertNotNull(callback.failure); 324 | assertTrue(callback.failure instanceof RuntimeException); 325 | assertEquals(error, callback.failure.getMessage()); 326 | 327 | // onSuccess() was not called. 328 | assertNull(callback.params); 329 | } 330 | 331 | private static class MockAuth extends Auth { 332 | 333 | private boolean loggedInViaPopup; 334 | private String lastUrl; 335 | 336 | private static final TokenStore TOKEN_STORE = new InMemoryTokenStore(); 337 | 338 | MockAuth() { 339 | super(TOKEN_STORE, new MockClock(), new StubScheduler()); 340 | TOKEN_STORE.clear(); 341 | } 342 | 343 | @Override 344 | void doLogin(String authUrl, Callback, Throwable> callback) { 345 | loggedInViaPopup = true; 346 | lastUrl = authUrl; 347 | } 348 | } 349 | 350 | private static class MockClock implements Auth.Clock { 351 | 352 | private static double now = 5000; 353 | 354 | @Override 355 | public double now() { 356 | return now; 357 | } 358 | } 359 | 360 | private static class InMemoryTokenStore implements TokenStore { 361 | 362 | private final Map store = new HashMap<>(); 363 | 364 | @Override 365 | public void set(String key, String value) { 366 | store.put(key, value); 367 | } 368 | 369 | @Override 370 | public String get(String key) { 371 | return store.get(key); 372 | } 373 | 374 | @Override 375 | public void remove(String key) { 376 | store.remove(key); 377 | } 378 | 379 | @Override 380 | public Set keySet() { 381 | return store.keySet(); 382 | } 383 | } 384 | 385 | private static class MockCallback implements Callback, Throwable> { 386 | 387 | private Map params; 388 | private Throwable failure; 389 | 390 | @Override 391 | public void onSuccess(Map params) { 392 | this.params = params; 393 | } 394 | 395 | @Override 396 | public void onFailure(Throwable caught) { 397 | failure = caught; 398 | } 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.freddyboucher 8 | gwt-oauth2-root 9 | 1.3-SNAPSHOT 10 | pom 11 | 12 | 13 | gwt-oauth2 14 | sample 15 | 16 | 17 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | sample 2 | ========== 3 | 4 | * [sample-client](sample-client) 5 | * [sample-shared](sample-shared) 6 | * [sample-server](sample-server) 7 | 8 | Live demo: [https://gwt-oauth2.appspot.com/](https://gwt-oauth2.appspot.com/) 9 | 10 | Quick start 11 | ------------- 12 | First of all, under the root sample module, run: 13 | ```mvn 14 | mvn clean install -Denv=dev 15 | ``` 16 | 17 | Then open 2 terminals: 18 | 19 | - In the first one, under the [sample-server](sample-server) module, run: 20 | ```mvn 21 | mvn appengine:devserver -am -Denv=dev 22 | ``` 23 | 24 | - In the second one, under the root sample module, run: 25 | ```mvn 26 | mvn gwt:codeserver -pl sample-client -am -Denv=dev 27 | ``` 28 | 29 | Then access http://localhost:8080/ in your browser. -------------------------------------------------------------------------------- /sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.freddyboucher.gwt.oauth2 8 | sample 9 | 1.3-SNAPSHOT 10 | pom 11 | 12 | 13 | sample-client 14 | sample-shared 15 | sample-server 16 | 17 | 18 | 19 | 1.8 20 | 1.8 21 | 22 | UTF-8 23 | UTF-8 24 | 25 | 26 | 27 | 28 | 29 | com.google.gwt 30 | gwt 31 | 2.9.0 32 | pom 33 | import 34 | 35 | 36 | javax.servlet 37 | javax.servlet-api 38 | 3.1.0 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-checkstyle-plugin 48 | 3.1.1 49 | 50 | google_checks.xml 51 | UTF-8 52 | true 53 | true 54 | false 55 | 0 56 | 57 | 58 | 59 | validate 60 | 61 | check 62 | 63 | 64 | 65 | 66 | 67 | com.puppycrawl.tools 68 | checkstyle 69 | 8.30 70 | 71 | 72 | 73 | 74 | net.ltgt.gwt.maven 75 | gwt-maven-plugin 76 | false 77 | 78 | 79 | 80 | 81 | 82 | maven-compiler-plugin 83 | 3.8.1 84 | 85 | 86 | net.ltgt.gwt.maven 87 | gwt-maven-plugin 88 | 1.0.0 89 | true 90 | 91 | 1.8 92 | true 93 | ${project.basedir}/sample-server/target/sample-server-${project.version}/ 94 | 95 | ${project.basedir}/sample-client/target/gwt/codeserver 96 | 97 | 98 | 99 | 100 | maven-source-plugin 101 | 3.2.1 102 | 103 | 104 | attach-sources 105 | package 106 | 107 | jar-no-fork 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 120 | env-prod 121 | 122 | true 123 | 124 | 125 | 126 | env-dev 127 | 128 | 129 | env 130 | dev 131 | 132 | 133 | 134 | ../gwt-oauth2 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /sample/sample-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | io.github.freddyboucher.gwt.oauth2 9 | sample 10 | 1.3-SNAPSHOT 11 | .. 12 | 13 | 14 | sample-client 15 | gwt-app 16 | 17 | 18 | 19 | ${project.groupId} 20 | sample-shared 21 | ${project.version} 22 | 23 | 24 | ${project.groupId} 25 | sample-shared 26 | ${project.version} 27 | sources 28 | 29 | 30 | com.google.gwt 31 | gwt-user 32 | 33 | 34 | com.google.gwt 35 | gwt-dev 36 | 37 | 38 | io.github.freddyboucher 39 | gwt-oauth2 40 | 1.3-SNAPSHOT 41 | gwt-lib 42 | 43 | 44 | 45 | 46 | 47 | 48 | net.ltgt.gwt.maven 49 | gwt-maven-plugin 50 | 51 | io.github.freddyboucher.gwt.oauth2.sample.App 52 | app 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /sample/sample-client/src/main/java/io/github/freddyboucher/gwt/oauth2/sample/App.java: -------------------------------------------------------------------------------- 1 | package io.github.freddyboucher.gwt.oauth2.sample; 2 | 3 | import com.google.gwt.core.client.Callback; 4 | import com.google.gwt.core.client.EntryPoint; 5 | import com.google.gwt.core.client.GWT; 6 | import com.google.gwt.event.dom.client.ClickHandler; 7 | import com.google.gwt.user.client.rpc.AsyncCallback; 8 | import com.google.gwt.user.client.ui.Anchor; 9 | import com.google.gwt.user.client.ui.Button; 10 | import com.google.gwt.user.client.ui.DialogBox; 11 | import com.google.gwt.user.client.ui.Grid; 12 | import com.google.gwt.user.client.ui.HasVerticalAlignment; 13 | import com.google.gwt.user.client.ui.Label; 14 | import com.google.gwt.user.client.ui.RootPanel; 15 | import com.google.gwt.user.client.ui.VerticalPanel; 16 | import io.github.freddyboucher.gwt.oauth2.client.Auth; 17 | import io.github.freddyboucher.gwt.oauth2.client.AuthRequest; 18 | import java.util.Map; 19 | import java.util.logging.Level; 20 | import java.util.logging.Logger; 21 | 22 | public class App implements EntryPoint { 23 | 24 | private static final Oauth2ServiceAsync GREETING_SERVICE = GWT.create(Oauth2Service.class); 25 | private static final Logger LOGGER = Logger.getLogger(App.class.getName()); 26 | private static final String GOOGLE_CLIENT_ID = 27 | "521140962557-s8135bi5j7gla15u3qtljvm0geumd55s.apps.googleusercontent.com"; 28 | private static final String FACEBOOK_CLIENT_ID = "916624668730153"; 29 | private static final String INSTAGRAM_CLIENT_ID = "1159265561099025"; 30 | private static final String MICROSOFT_CLIENT_ID = "58e9880d-a64f-48d2-9f59-f0cc19fbe536"; 31 | private static final String APPLE_CLIENT_ID = "com.appspot.gwt-oauth2.client"; 32 | private static final String AMAZON_COGNITO_CLIENT_ID = "3uqob948bbcrr09v9gfoppdkh3"; 33 | 34 | @Override 35 | public void onModuleLoad() { 36 | VerticalPanel panel = new VerticalPanel(); 37 | panel.setSpacing(10); 38 | 39 | panel.add(new Button("Google", (ClickHandler) event -> { 40 | AuthRequest req = 41 | new AuthRequest("https", "accounts.google.com", "o/oauth2/auth", GOOGLE_CLIENT_ID) 42 | .setParameter("scope", "email profile openid"); 43 | Auth.get().login(req, createCallback(), "access_token"); 44 | })); 45 | 46 | panel.add(new Button("Facebook", (ClickHandler) event -> { 47 | AuthRequest req = 48 | new AuthRequest("https", "www.facebook.com", "dialog/oauth", FACEBOOK_CLIENT_ID) 49 | .setParameter("scope", "public_profile,email"); 50 | Auth.get().login(req, createCallback(), "access_token"); 51 | })); 52 | 53 | panel.add(new Button("Instagram", (ClickHandler) event -> { 54 | AuthRequest req = 55 | new AuthRequest("https", "api.instagram.com", "oauth/authorize", INSTAGRAM_CLIENT_ID) 56 | .setParameter("scope", "user_profile,user_media") 57 | .setParameter("response_type", "code"); 58 | Auth.get().login(req, createCallback(), "code"); 59 | })); 60 | 61 | panel.add(new Button("Microsoft", (ClickHandler) event -> { 62 | AuthRequest req = 63 | new AuthRequest("https", "login.microsoftonline.com", "common/oauth2/v2.0/authorize/", 64 | MICROSOFT_CLIENT_ID).setParameter("scope", "email profile openid"); 65 | Auth.get().login(req, createCallback(), "access_token"); 66 | })); 67 | 68 | panel.add(new Button("Apple (no scope)", (ClickHandler) event -> { 69 | AuthRequest req = 70 | new AuthRequest("https", "appleid.apple.com", "auth/authorize", APPLE_CLIENT_ID) 71 | .setParameter("response_type", "code").setParameter("state", "my_custom_state"); 72 | Auth.get().login(req, createCallback(), "code"); 73 | })); 74 | 75 | panel.add(new Button("Apple (name/email scope)", (ClickHandler) event -> { 76 | AuthRequest req = 77 | new AuthRequest("https", "appleid.apple.com", "auth/authorize", APPLE_CLIENT_ID) 78 | .setParameter("scope", "name email").setParameter("response_type", "code") 79 | .setParameter("state", "my_custom_state").setParameter("response_mode", "form_post") 80 | .setParameter("redirect_uri", GWT.getModuleBaseURL() + "oauthWindow.jsp"); 81 | Auth.get().login(req, createCallback(), "code"); 82 | })); 83 | 84 | panel.add(new Button("Amazon Cognito", (ClickHandler) event -> { 85 | AuthRequest req = 86 | new AuthRequest("https", "gwt-oauth2.auth.us-east-2.amazoncognito.com", "login", 87 | AMAZON_COGNITO_CLIENT_ID) 88 | .setParameter("scope", "email").setParameter("response_type", "code"); 89 | Auth.get().login(req, createCallback()); 90 | })); 91 | 92 | Button clearBtn = new Button("Clear All Tokens"); 93 | clearBtn.addClickHandler(event -> Auth.get().clearAllTokens()); 94 | panel.add(clearBtn); 95 | 96 | Anchor privacyPolicy = new Anchor("Privacy Policy"); 97 | privacyPolicy.setHref( 98 | "https://www.privacypolicygenerator.info/live.php?token=BYr9WP5smDs5lKjfoyjy7XrTGEMcxYtv"); 99 | privacyPolicy.setTarget("_blank"); 100 | panel.add(privacyPolicy); 101 | 102 | RootPanel.get().add(panel); 103 | } 104 | 105 | private static Callback, Throwable> createCallback() { 106 | return new Callback, Throwable>() { 107 | @Override 108 | public void onFailure(Throwable throwable) { 109 | showThrowable(throwable); 110 | } 111 | 112 | @Override 113 | public void onSuccess(Map result) { 114 | LOGGER.log(Level.INFO, result.toString()); 115 | DialogBox box = new DialogBox(true, false); 116 | box.setGlassEnabled(true); 117 | box.setWidth("700px"); 118 | box.setText("Response"); 119 | Grid grid = new Grid(result.size(), 2); 120 | grid.getElement().getStyle().setProperty("wordBreak", "break-all"); 121 | for (int i = 0; i < result.size(); i++) { 122 | Map.Entry entry = result.entrySet().stream().skip(i).findFirst() 123 | .orElseThrow(IllegalStateException::new); 124 | grid.setText(i, 0, entry.getKey()); 125 | grid.getCellFormatter().setVerticalAlignment(i, 0, HasVerticalAlignment.ALIGN_TOP); 126 | grid.setText(i, 1, entry.getValue()); 127 | } 128 | grid.getColumnFormatter().setWidth(0, "200px"); 129 | box.setWidget(grid); 130 | box.center(); 131 | GREETING_SERVICE.validate(result.get("access_token"), new AsyncCallback() { 132 | @Override 133 | public void onFailure(Throwable throwable) { 134 | showThrowable(throwable); 135 | } 136 | 137 | @Override 138 | public void onSuccess(Boolean result) { 139 | LOGGER.info("Backend validation response: " + result); 140 | } 141 | }); 142 | } 143 | }; 144 | } 145 | 146 | private static void showThrowable(Throwable throwable) { 147 | LOGGER.log(Level.SEVERE, null, throwable); 148 | DialogBox box = new DialogBox(true, false); 149 | box.setGlassEnabled(true); 150 | box.setText("Error"); 151 | box.setWidget(new Label(throwable.getMessage())); 152 | box.center(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /sample/sample-client/src/main/module.gwt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/sample-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | io.github.freddyboucher.gwt.oauth2 9 | sample 10 | 1.3-SNAPSHOT 11 | .. 12 | 13 | 14 | sample-server 15 | war 16 | 17 | 18 | 19 | ${project.groupId} 20 | sample-shared 21 | ${project.version} 22 | 23 | 24 | com.google.gwt 25 | gwt-servlet 26 | 27 | 28 | javax.servlet 29 | javax.servlet-api 30 | provided 31 | 32 | 33 | org.apache.httpcomponents 34 | httpclient 35 | 4.5.13 36 | 37 | 38 | 39 | 40 | 41 | 42 | com.google.appengine 43 | appengine-maven-plugin 44 | 1.9.84 45 | 46 | snapshot 47 | 8080 48 | gwt-oauth2 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | env-prod 59 | 60 | true 61 | 62 | 63 | 64 | ${project.groupId} 65 | sample-client 66 | ${project.version} 67 | war 68 | runtime 69 | 70 | 71 | 72 | 73 | env-dev 74 | 75 | 76 | env 77 | dev 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /sample/sample-server/src/main/java/io/github/freddyboucher/gwt/oauth2/sample/Oauth2ServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.freddyboucher.gwt.oauth2.sample; 2 | 3 | import com.google.gwt.user.server.rpc.RemoteServiceServlet; 4 | import java.util.logging.Logger; 5 | import javax.servlet.annotation.WebServlet; 6 | 7 | @WebServlet("/app/oauth2") 8 | public class Oauth2ServiceImpl extends RemoteServiceServlet implements Oauth2Service { 9 | 10 | private static final Logger LOGGER = Logger.getAnonymousLogger(); 11 | 12 | @Override 13 | public boolean validate(String token) { 14 | LOGGER.info(token); 15 | return true; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample/sample-server/src/main/webapp/WEB-INF/appengine-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | java8 4 | 5 | 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | false 22 | 23 | false 24 | 25 | 1 26 | 27 | 28 | -------------------------------------------------------------------------------- /sample/sample-server/src/main/webapp/WEB-INF/logging.properties: -------------------------------------------------------------------------------- 1 | # A default java.util.logging configuration. 2 | # (All App Engine logging is through java.util.logging by default). 3 | # 4 | # To use this configuration, copy it into your application's WEB-INF 5 | # folder and add the following to your appengine-web.xml: 6 | # 7 | # 8 | # 9 | # 10 | # 11 | # Set the default logging level for all loggers to WARNING 12 | .level=INFO 13 | -------------------------------------------------------------------------------- /sample/sample-server/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | OauthWindow 9 | /app/oauthWindow.jsp 10 | 11 | compilerSourceVM 12 | 1.8 13 | 14 | 15 | compilerTargetVM 16 | 1.8 17 | 18 | 19 | 20 | OauthWindow 21 | /app/oauthWindow.jsp 22 | 23 | 24 | 25 | index.html 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sample/sample-server/src/main/webapp/app/oauthWindow.jsp: -------------------------------------------------------------------------------- 1 | <%@ page import="java.util.Arrays" %> 2 | <%@ page import="org.apache.http.client.utils.URIBuilder" %> 3 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 4 | <%@ page session="false" %> 5 | 6 | 7 | 8 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sample/sample-server/src/main/webapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freddyboucher/gwt-oauth2/0d5a353c245a0dc50af072039e8273bc0711120b/sample/sample-server/src/main/webapp/favicon.ico -------------------------------------------------------------------------------- /sample/sample-server/src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | gwt-oauth2 7 | 8 | 9 | 10 |

13 | Your web browser must have JavaScript enabled in order for this application to display 14 | correctly. 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample/sample-server/src/main/webapp/sample.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freddyboucher/gwt-oauth2/0d5a353c245a0dc50af072039e8273bc0711120b/sample/sample-server/src/main/webapp/sample.css -------------------------------------------------------------------------------- /sample/sample-shared/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | io.github.freddyboucher.gwt.oauth2 9 | sample 10 | 1.3-SNAPSHOT 11 | .. 12 | 13 | 14 | sample-shared 15 | 16 | 17 | 18 | com.google.gwt 19 | gwt-servlet 20 | provided 21 | 22 | 23 | 24 | 25 | 26 | 27 | maven-source-plugin 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample/sample-shared/src/main/java/io/github/freddyboucher/gwt/oauth2/sample/Oauth2Service.java: -------------------------------------------------------------------------------- 1 | package io.github.freddyboucher.gwt.oauth2.sample; 2 | 3 | import com.google.gwt.user.client.rpc.RemoteService; 4 | import com.google.gwt.user.client.rpc.RemoteServiceRelativePath; 5 | 6 | @RemoteServiceRelativePath("oauth2") 7 | public interface Oauth2Service extends RemoteService { 8 | 9 | boolean validate(String token); 10 | } 11 | -------------------------------------------------------------------------------- /sample/sample-shared/src/main/java/io/github/freddyboucher/gwt/oauth2/sample/Oauth2ServiceAsync.java: -------------------------------------------------------------------------------- 1 | package io.github.freddyboucher.gwt.oauth2.sample; 2 | 3 | import com.google.gwt.user.client.rpc.AsyncCallback; 4 | 5 | public interface Oauth2ServiceAsync { 6 | 7 | void validate(String token, AsyncCallback callback); 8 | } 9 | --------------------------------------------------------------------------------