├── .gitignore ├── .sonarcloud.properties ├── .travis.yml ├── LICENSE ├── pom.xml ├── readme.md └── src ├── main └── java │ └── com │ └── idmworks │ └── security │ └── google │ ├── AccessTokenInfo.java │ ├── GoogleApiUtils.java │ ├── GoogleOAuthCallbackHandler.java │ ├── GoogleOAuthServerAuthModule.java │ ├── GoogleUserInfoCallBack.java │ ├── LoginContextWrapper.java │ ├── ParseUtils.java │ ├── StateHelper.java │ └── api │ ├── GoogleOAuthPrincipal.java │ └── GoogleUserInfo.java └── test ├── java └── com │ └── idmworks │ └── security │ └── google │ └── ParseUtilsTest.java └── resources └── test.config /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | - oraclejdk7 5 | - openjdk7 6 | - openjdk6 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2012 Phillip Green II 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.idmworks.security 6 | google-oauth-2_0-sam 7 | 0.1.0-SNAPSHOT 8 | 9 | 10 | UTF-8 11 | 12 | 13 | 14 | 15 | 16 | org.apache.maven.plugins 17 | maven-compiler-plugin 18 | 2.3.2 19 | 20 | 1.6 21 | 1.6 22 | 23 | 24 | 25 | org.apache.maven.plugins 26 | maven-surefire-plugin 27 | 2.12 28 | 29 | 30 | **/*Test.java 31 | **/*Spec.java 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | repository.jboss.org 40 | JBoss Repository 41 | http://repository.jboss.org/nexus/content/groups/public-jboss/ 42 | 43 | 44 | 45 | 46 | junit 47 | junit 48 | 4.13.1 49 | test 50 | 51 | 52 | org.mockito 53 | mockito-core 54 | 1.9.0 55 | test 56 | 57 | 58 | org.jboss.spec 59 | jboss-javaee-6.0 60 | 3.0.0.Final 61 | pom 62 | provided 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | [![Build Status](https://travis-ci.org/phillipgreenii/google-oauth-2.0-serverauthmodule.svg?branch=master)](https://travis-ci.org/phillipgreenii/google-oauth-2.0-serverauthmodule) 4 | 5 | Google OAuth 2.0 ServerAuthModule is a ServerAuthModule (SAM), [JSR-196 (JASPIC) Spec][jsr-196], implementation of [Google OAuth 2.0][google-oauth]: `com.idmworks.security.google.GoogleOAuthServerAuthModule`. It optionally supports the [LoginModule Bridge Profile]. 6 | 7 | Installation 8 | ============ 9 | 10 | Copy `google-oauth-2_0-sam-0.1.x.jar` into the class path of the application server. See [Installation](https://bitbucket.org/phillip_green_idmworks/google-oauth-2.0-serverauthmodule/wiki/setup/1-installation) for application server specific instructions. 11 | 12 | 13 | Configuration 14 | ============= 15 | 16 | Before you can authenticate with Google OAuth, you will need to create a Client ID for your web application at [Client ID API Console][google-api-console]. 17 | 18 | Next, the GoogleOAuthServerAuthModule needs added to the application server. See [Configuration](https://bitbucket.org/phillip_green_idmworks/google-oauth-2.0-serverauthmodule/wiki/setup/2-configuration) for application server specific instructions. 19 | 20 | 21 | ### GoogleOAuthServerAuthModule 22 | 23 | The following attributes can be used to configure `com.idmworks.security.google.GoogleOAuthServerAuthModule`. 24 | 25 | #### `oauth.clientid` (_REQUIRED_) 26 | `oauth.clientid` must be set to a "`Client ID`" from [Client ID API Console][google-api-console]. 27 | 28 | #### `oauth.clientsecret` (_REQUIRED_) 29 | `oauth.clientsecret` must be set to the "`Client Secret`" from [Client ID API Console][google-api-console] of the "`Client ID`" specified in `oauth.clientid`. 30 | 31 | 32 | #### `oauth.endpoint` (_optional_) 33 | default: `https://accounts.google.com/o/oauth2/auth` 34 | 35 | `oauth.endpoint` is the URI that will be connect to for the OAuth authentication (Google). 36 | 37 | #### `oauth.callback_uri` (_optional_) 38 | default: `/j_oauth_callback` 39 | 40 | `oauth.callback_uri` is the URI that Google will redirect to after the user responds to the request. This should correspond to "`Redirect URIs`" value defined in the [Client ID API Console][google-api-console]. 41 | 42 | #### `javax.security.auth.login.LoginContext` (_optional_) 43 | default: `"com.idmworks.security.google.GoogleOAuthServerAuthModule"` 44 | 45 | With [LoginModule Bridge Profile], `javax.security.auth.login.LoginContext` is where you define the name of the [LoginContext][javadocs-logincontext] to use. 46 | 47 | #### `ignore_missing_login_context` (_optional_) 48 | default: `"false"` 49 | 50 | `GoogleOAuthServerAuthModule` is configured by default to support the [LoginModule Bridge Profile]. If you set `ignore_missing_login_context` to true (in the case when you don't want to use any [LoginModules][javadocs-loginmodule]), there will be no error when a LoginContext isn't found. 51 | 52 | 53 | #### `add_domain_as_group` (_optional_) 54 | default: `"false"` 55 | 56 | If `add_domain_as_group` is `true`, then the domain of the email address of the authenticated user will be added as a group. IE: "idmworks.com" will be a principal added as a group for the user "phillip.green@idmworks.com". 57 | 58 | 59 | 60 | #### `default_groups` (_optional_) 61 | default: `""` 62 | 63 | `default_groups` is a comma (",") separated list of groups that will be given to the principal upon successful authentication. 64 | 65 | Usage 66 | ===== 67 | 68 | The configured `GoogleOAuthServerAuthModule` needs specified in the application server specific configuration for each application. See [Usage](https://bitbucket.org/phillip_green_idmworks/google-oauth-2.0-serverauthmodule/wiki/setup/3-usage) for application server specific instructions. 69 | 70 | Common Problems 71 | =============== 72 | See [Common Problems](https://bitbucket.org/phillip_green_idmworks/google-oauth-2.0-serverauthmodule/wiki/common-problems). 73 | 74 | 75 | References 76 | ========== 77 | + [JSR-196][jsr-196] 78 | + [Google API Console][google-api-console] 79 | + [Google OAuth][google-oauth] 80 | + [Google OAuth for Webservers][google-oauth-webserver] 81 | + [LoginContext Javadocs][javadocs-logincontext] 82 | + [LoginModule Javadocs][javadocs-loginmodule] 83 | + [LoginModule Bridge Profile in glassfish][LoginModule Bridge Profile] 84 | + [LoginContext Configuration][configuration-logincontext] 85 | + [configuration-logincontext] 86 | + [openid4java-jsr196] 87 | + [Project Source Code on Bitbucket][bitbucket-source] 88 | 89 | [jsr-196]: http://www.jcp.org/en/jsr/detail?id=196 90 | [google-api-console]: https://code.google.com/apis/console/ 91 | [google-oauth]: https://developers.google.com/accounts/docs/OAuth2 92 | [google-oauth-webserver]: https://developers.google.com/accounts/docs/OAuth2WebServer 93 | [javadocs-logincontext]: http://docs.oracle.com/javase/6/docs/api/javax/security/auth/login/LoginContext.html 94 | [javadocs-loginmodule]: http://docs.oracle.com/javase/6/docs/api/javax/security/auth/spi/LoginModule.html 95 | [LoginModule Bridge Profile]: https://blogs.oracle.com/nasradu8/entry/loginmodule_bridge_profile_jaspic_in 96 | [configuration-logincontext]: http://docs.oracle.com/javase/6/docs/api/javax/security/auth/login/Configuration.html 97 | [openid4java-jsr196]: http://code.google.com/p/openid4java-jsr196/ 98 | [bitbucket-source]: https://bitbucket.org/phillip_green_idmworks/gooogle-oauth-2.0-serverauthmodule 99 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/AccessTokenInfo.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * Information about access token; 7 | * 8 | * @author pdgreen 9 | */ 10 | public class AccessTokenInfo { 11 | 12 | private final String accessToken; 13 | private final Date expiration; 14 | private final String type; 15 | 16 | public AccessTokenInfo(String accessToken, Date expiration, String type) { 17 | this.accessToken = accessToken; 18 | this.expiration = expiration; 19 | this.type = type; 20 | } 21 | 22 | public String getAccessToken() { 23 | return accessToken; 24 | } 25 | 26 | public Date getExpiration() { 27 | return expiration; 28 | } 29 | 30 | public String getType() { 31 | return type; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return getAccessToken(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/GoogleApiUtils.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google; 2 | 3 | import com.idmworks.security.google.api.GoogleUserInfo; 4 | import java.io.BufferedReader; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStream; 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | import javax.net.ssl.HttpsURLConnection; 13 | 14 | /** 15 | * Methods of this class connect to Google APIs.
Instead of including an external library to handle Google APIs, 16 | * and making installation/packaging more difficult, I created a couple small methods to handle it. There is only a 17 | * couple API calls to parse, so I felt I could by pass an external library for the sake of easier installation. 18 | * 19 | * @author pdgreen 20 | */ 21 | public class GoogleApiUtils { 22 | /* 23 | * User Info API 24 | */ 25 | 26 | public static final String USERINFO_API_PERMISSION_EMAIL = "https://www.googleapis.com/auth/userinfo.email"; 27 | public static final String USERINFO_API_PERMISSION_PROFILE = "https://www.googleapis.com/auth/userinfo.profile"; 28 | public static final String USERINFO_API_URI = "https://www.googleapis.com/oauth2/v1/userinfo"; 29 | /* 30 | * parameters 31 | */ 32 | public static final String USERINFO_API_ID_PARAMETER = "id"; 33 | public static final String USERINFO_API_EMAIL_PARAMETER = "email"; 34 | public static final String USERINFO_API_VERIFIED_EMAIL_PARAMETER = "verified_email"; 35 | public static final String USERINFO_API_NAME_PARAMETER = "name"; 36 | public static final String USERINFO_API_GIVEN_NAME_PARAMETER = "given_name"; 37 | public static final String USERINFO_API_FAMILY_NAME_PARAMETER = "family_name"; 38 | public static final String USERINFO_API_GENDER_PARAMETER = "gender"; 39 | public static final String USERINFO_API_LINK_PARAMETER = "link"; 40 | public static final String USERINFO_API_PICTURE_PARAMETER = "picture"; 41 | public static final String USERINFO_API_LOCALE_PARAMETER = "locale"; 42 | /* 43 | * User Token API 44 | */ 45 | public static final String TOKEN_API_URI = "https://accounts.google.com/o/oauth2/token"; 46 | public static final String TOKEN_API_URI_DEFAULT_ENDPOINT = "https://accounts.google.com/o/oauth2/auth"; 47 | /* 48 | * parameters 49 | */ 50 | public static final String TOKEN_API_ACCESS_TYPE_PARAMETER = "access_type"; 51 | public static final String TOKEN_API_ACCESS_TOKEN_PARAMETER = "access_token"; 52 | public static final String TOKEN_API_APPROVAL_PROMPT_PARAMETER = "approval_prompt"; 53 | public static final String TOKEN_API_CLIENT_ID_PARAMETER = "client_id"; 54 | public static final String TOKEN_API_CLIENT_SECRET_PARAMETER = "client_secret"; 55 | public static final String TOKEN_API_CODE_PARAMETER = "code"; 56 | public static final String TOKEN_API_ERROR_PARAMETER = "error"; 57 | public static final String TOKEN_API_EXPIRES_IN_PARAMETER = "expires_in"; 58 | public static final String TOKEN_API_GRANT_TYPE_PARAMETER = "grant_type"; 59 | public static final String TOKEN_API_REDIRECT_URI_PARAMETER = "redirect_uri"; 60 | public static final String TOKEN_API_RESPONSE_TYPE_PARAMETER = "response_type"; 61 | public static final String TOKEN_API_SCOPE_PARAMETER = "scope"; 62 | public static final String TOKEN_API_STATE_PARAMETER = "state"; 63 | public static final String TOKEN_API_TOKEN_TYPE_PARAMETER = "token_type"; 64 | /* 65 | * values 66 | */ 67 | public static final String TOKEN_API_AUTHORIZATION_CODE_VALUE = "authorization_code"; 68 | private static final Logger LOGGER = Logger.getLogger(GoogleApiUtils.class.getName()); 69 | 70 | public static URI buildOauthUri(final String redirectUri, final URI endpoint, final String clientid) { 71 | 72 | final StringBuilder querySb = new StringBuilder(); 73 | querySb.append(TOKEN_API_SCOPE_PARAMETER).append("=").append(USERINFO_API_PERMISSION_EMAIL).append(" ").append(USERINFO_API_PERMISSION_PROFILE); 74 | querySb.append("&"); 75 | querySb.append(TOKEN_API_REDIRECT_URI_PARAMETER).append("=").append(redirectUri); 76 | querySb.append("&"); 77 | querySb.append(TOKEN_API_RESPONSE_TYPE_PARAMETER).append("=").append(TOKEN_API_CODE_PARAMETER); 78 | querySb.append("&"); 79 | querySb.append(TOKEN_API_CLIENT_ID_PARAMETER).append("=").append(clientid); 80 | 81 | final String totalQuery = endpoint.getQuery() == null ? querySb.toString() : endpoint.getQuery() + "&" + querySb.toString(); 82 | try { 83 | return new URI(endpoint.getScheme(), endpoint.getUserInfo(), endpoint.getHost(), endpoint.getPort(), endpoint.getPath(), totalQuery, endpoint.getFragment()); 84 | } catch (URISyntaxException ex) { 85 | throw new IllegalArgumentException("Unable to build Oauth Uri", ex); 86 | } 87 | } 88 | 89 | static Response sendRequest(final String method, final URI destination, final String body) { 90 | if (LOGGER.isLoggable(Level.FINER)) { 91 | LOGGER.log(Level.FINER, "sendRequest({0},{1},{2})", new Object[]{method, destination, "hasBody?" + body != null}); 92 | } 93 | 94 | HttpsURLConnection httpsURLConnection; 95 | 96 | try { 97 | httpsURLConnection = (HttpsURLConnection) destination.toURL().openConnection(); 98 | httpsURLConnection.setRequestMethod(method); 99 | if (body != null) { 100 | httpsURLConnection.setDoOutput(true); 101 | } 102 | httpsURLConnection.connect(); 103 | } catch (IOException ex) { 104 | throw new IllegalStateException("Unable to create connection", ex); 105 | } 106 | if (body 107 | != null) { 108 | try { 109 | final OutputStream out = httpsURLConnection.getOutputStream(); 110 | LOGGER.log(Level.FINER, "body: {0}", new Object[]{body}); 111 | out.write(body.getBytes()); 112 | out.flush(); 113 | out.close(); 114 | } catch (IOException ex) { 115 | throw new IllegalStateException("Unable to write body", ex); 116 | } 117 | } 118 | 119 | 120 | try { 121 | final int status = httpsURLConnection.getResponseCode(); 122 | LOGGER.log(Level.FINER, "response code: {0}", new Object[]{status}); 123 | 124 | final String responseBody; 125 | if (status < 400) { 126 | final BufferedReader reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream())); 127 | final StringBuilder stringBuilder = new StringBuilder(); 128 | String line = null; 129 | while ((line = reader.readLine()) != null) { 130 | stringBuilder.append(line).append("\n"); 131 | } 132 | reader.close(); 133 | responseBody = stringBuilder.toString(); 134 | } else { 135 | responseBody = null; 136 | } 137 | 138 | return new Response(status, responseBody); 139 | } catch (IOException ex) { 140 | throw new IllegalStateException("Unable to read response", ex); 141 | } 142 | } 143 | 144 | static class Response { 145 | 146 | private final int status; 147 | private final String body; 148 | 149 | public Response(int status, String body) { 150 | this.status = status; 151 | this.body = body; 152 | } 153 | 154 | public String getBody() { 155 | return body; 156 | } 157 | 158 | public int getStatus() { 159 | return status; 160 | } 161 | } 162 | 163 | static Response GET(final URI destination) { 164 | return sendRequest("GET", destination, null); 165 | } 166 | 167 | static Response POST(final URI destination, final String body) { 168 | return sendRequest("POST", destination, body); 169 | } 170 | 171 | public static AccessTokenInfo lookupAccessTokenInfo(String redirectUri, String authorizationCode, String clientid, String clientSecret) { 172 | //FIXME cache URI 173 | final URI apiUri; 174 | try { 175 | apiUri = new URI(TOKEN_API_URI); 176 | } catch (URISyntaxException ex) { 177 | throw new IllegalStateException("unable to create uri for " + TOKEN_API_URI, ex); 178 | } 179 | 180 | final StringBuilder bodySb = new StringBuilder(); 181 | bodySb.append(TOKEN_API_CODE_PARAMETER).append("=").append(authorizationCode); 182 | bodySb.append("&"); 183 | bodySb.append(TOKEN_API_CLIENT_ID_PARAMETER).append("=").append(clientid); 184 | bodySb.append("&"); 185 | bodySb.append(TOKEN_API_CLIENT_SECRET_PARAMETER).append("=").append(clientSecret); 186 | bodySb.append("&"); 187 | bodySb.append(TOKEN_API_REDIRECT_URI_PARAMETER).append("=").append(redirectUri); 188 | bodySb.append("&"); 189 | bodySb.append(TOKEN_API_GRANT_TYPE_PARAMETER).append("=").append(TOKEN_API_AUTHORIZATION_CODE_VALUE); 190 | LOGGER.log(Level.FINE, "Lookup Access Token body: {0}", bodySb); 191 | 192 | final Response response = POST(apiUri, bodySb.toString()); 193 | 194 | if (response.getStatus() == 200) { 195 | return ParseUtils.parseAccessTokenJson(response.getBody()); 196 | } else { 197 | return null;//FIXME handle this better 198 | } 199 | } 200 | 201 | public static GoogleUserInfo retrieveGoogleUserInfo(AccessTokenInfo accessTokenInfo) { 202 | 203 | final URI apiUri; 204 | try { 205 | apiUri = new URI(new StringBuilder(USERINFO_API_URI).append("?").append(TOKEN_API_ACCESS_TOKEN_PARAMETER).append("=").append(accessTokenInfo.getAccessToken()).toString()); 206 | } catch (URISyntaxException ex) { 207 | throw new IllegalStateException("unable to create uri for " + USERINFO_API_URI, ex); 208 | } 209 | 210 | final Response response = GET(apiUri); 211 | 212 | if (response.getStatus() == 200) { 213 | return ParseUtils.parseGoogleUserInfoJson(response.getBody()); 214 | } else { 215 | return null;//FIXME handle this better 216 | } 217 | 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/GoogleOAuthCallbackHandler.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google; 2 | 3 | import com.idmworks.security.google.api.GoogleUserInfo; 4 | import java.io.IOException; 5 | import javax.security.auth.callback.*; 6 | 7 | /** 8 | * Callback handler for {@link GoogleOAuthServerAuthModule}. 9 | * 10 | * @author pdgreen 11 | */ 12 | public class GoogleOAuthCallbackHandler implements CallbackHandler { 13 | 14 | private GoogleUserInfo googleUserInfo; 15 | 16 | public GoogleOAuthCallbackHandler() { 17 | } 18 | 19 | public GoogleOAuthCallbackHandler(GoogleUserInfo googleUserInfo) { 20 | this.googleUserInfo = googleUserInfo; 21 | } 22 | 23 | public GoogleUserInfo getGoogleUserInfo() { 24 | return googleUserInfo; 25 | } 26 | 27 | public void setGoogleUserInfo(GoogleUserInfo googleUserInfo) { 28 | this.googleUserInfo = googleUserInfo; 29 | } 30 | 31 | @Override 32 | public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException { 33 | for (Callback callback : callbacks) { 34 | if (callback instanceof NameCallback) { 35 | ((NameCallback) callback).setName(googleUserInfo.getEmail()); 36 | } else if (callback instanceof PasswordCallback) { 37 | ((PasswordCallback) callback).setPassword(null); 38 | } else if (callback instanceof GoogleUserInfoCallBack) { 39 | ((GoogleUserInfoCallBack) callback).setGoogleUserInfo(googleUserInfo); 40 | } else { 41 | throw new UnsupportedCallbackException(callback); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/GoogleOAuthServerAuthModule.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google; 2 | 3 | import com.idmworks.security.google.api.GoogleOAuthPrincipal; 4 | import com.idmworks.security.google.api.GoogleUserInfo; 5 | import java.io.IOException; 6 | import java.net.URI; 7 | import java.net.URISyntaxException; 8 | import java.security.Principal; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | import javax.security.auth.Subject; 16 | import javax.security.auth.callback.Callback; 17 | import javax.security.auth.callback.CallbackHandler; 18 | import javax.security.auth.login.LoginContext; 19 | import javax.security.auth.login.LoginException; 20 | import javax.security.auth.message.AuthException; 21 | import javax.security.auth.message.AuthStatus; 22 | import javax.security.auth.message.MessageInfo; 23 | import javax.security.auth.message.MessagePolicy; 24 | import javax.security.auth.message.callback.CallerPrincipalCallback; 25 | import javax.security.auth.message.callback.GroupPrincipalCallback; 26 | import javax.security.auth.message.module.ServerAuthModule; 27 | import javax.servlet.http.HttpServletRequest; 28 | import javax.servlet.http.HttpServletResponse; 29 | 30 | /** 31 | * SAM ({@link ServerAuthModule}) for Google OAuth. 32 | * 33 | * @author pdgreen 34 | */ 35 | public class GoogleOAuthServerAuthModule implements ServerAuthModule { 36 | 37 | /* 38 | * SAM Constants 39 | */ 40 | private static final String LEARNING_CONTEXT_KEY = "javax.security.auth.login.LoginContext"; 41 | private static final String IS_MANDATORY_INFO_KEY = "javax.security.auth.message.MessagePolicy.isMandatory"; 42 | private static final String AUTH_TYPE_INFO_KEY = "javax.servlet.http.authType"; 43 | private static final String AUTH_TYPE_GOOGLE_OAUTH_KEY = "Google-OAuth"; 44 | /* 45 | * defaults 46 | */ 47 | public static final String DEFAULT_OAUTH_CALLBACK_PATH = "/j_oauth_callback"; 48 | /* 49 | * property names 50 | */ 51 | private static final String ENDPOINT_PROPERTY_NAME = "oauth.endpoint"; 52 | private static final String CLIENTID_PROPERTY_NAME = "oauth.clientid"; 53 | private static final String CLIENTSECRET_PROPERTY_NAME = "oauth.clientsecret"; 54 | private static final String CALLBACK_URI_PROPERTY_NAME = "oauth.callback_uri"; 55 | private static final String IGNORE_MISSING_LOGIN_CONTEXT = "ignore_missing_login_context"; 56 | private static final String ADD_DOMAIN_AS_GROUP = "add_domain_as_group"; 57 | private static final String DEFAULT_GROUPS_PROPERTY_NAME = "default_groups"; 58 | private static Logger LOGGER = Logger.getLogger(GoogleOAuthServerAuthModule.class.getName()); 59 | protected static final Class[] SUPPORTED_MESSAGE_TYPES = new Class[]{ 60 | javax.servlet.http.HttpServletRequest.class, 61 | javax.servlet.http.HttpServletResponse.class}; 62 | private CallbackHandler handler; 63 | //properties 64 | private String clientid; 65 | private String clientSecret; 66 | private URI endpoint; 67 | private String oauthAuthenticationCallbackUri; 68 | private boolean ignoreMissingLoginContext; 69 | private boolean addDomainAsGroup; 70 | private String defaultGroups; 71 | private GoogleOAuthCallbackHandler googleOAuthCallbackHandler; 72 | private LoginContextWrapper loginContextWrapper; 73 | 74 | String retrieveOptionalProperty(final Map properties, final String name, final String defaultValue) { 75 | LOGGER.log(Level.FINER, "retrieveOptionalProperty(_,{0},_)", name); 76 | if (properties.containsKey(name)) { 77 | return properties.get(name); 78 | } else { 79 | return defaultValue; 80 | } 81 | } 82 | 83 | String retrieveRequiredProperty(final Map properties, final String name) throws AuthException { 84 | LOGGER.log(Level.FINER, "retrieveRequiredProperty(_,{0})", name); 85 | if (properties.containsKey(name)) { 86 | return properties.get(name); 87 | } else { 88 | final String message = String.format("Required field '%s' not specified!", name); 89 | LOGGER.log(Level.SEVERE, message); 90 | throw new AuthException(message); 91 | } 92 | } 93 | 94 | @Override 95 | public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler handler, Map options) throws AuthException { 96 | LOGGER.log(Level.FINER, "initialize()"); 97 | this.handler = handler; 98 | //properties 99 | this.clientid = retrieveRequiredProperty(options, CLIENTID_PROPERTY_NAME); 100 | this.clientSecret = retrieveRequiredProperty(options, CLIENTSECRET_PROPERTY_NAME); 101 | try { 102 | this.endpoint = new URI(retrieveOptionalProperty(options, ENDPOINT_PROPERTY_NAME, GoogleApiUtils.TOKEN_API_URI_DEFAULT_ENDPOINT)); 103 | } catch (URISyntaxException ex) { 104 | final String message = String.format("Invalid field '%s'", ENDPOINT_PROPERTY_NAME); 105 | LOGGER.log(Level.SEVERE, message, ex); 106 | final AuthException aex = new AuthException(message); 107 | aex.initCause(ex); 108 | throw aex; 109 | } 110 | this.oauthAuthenticationCallbackUri = retrieveOptionalProperty(options, CALLBACK_URI_PROPERTY_NAME, DEFAULT_OAUTH_CALLBACK_PATH); 111 | this.ignoreMissingLoginContext = Boolean.parseBoolean(retrieveOptionalProperty(options, IGNORE_MISSING_LOGIN_CONTEXT, Boolean.toString(false))); 112 | this.addDomainAsGroup = Boolean.parseBoolean(retrieveOptionalProperty(options, ADD_DOMAIN_AS_GROUP, Boolean.toString(false))); 113 | this.defaultGroups = retrieveOptionalProperty(options, DEFAULT_GROUPS_PROPERTY_NAME, ""); 114 | final String learningContextName = retrieveOptionalProperty(options, LEARNING_CONTEXT_KEY, GoogleOAuthServerAuthModule.class.getName()); 115 | this.googleOAuthCallbackHandler = new GoogleOAuthCallbackHandler(); 116 | this.loginContextWrapper = new LoginContextWrapper(createLoginContext(learningContextName, googleOAuthCallbackHandler)); 117 | 118 | LOGGER.log(Level.FINE, "{0} initialized", new Object[]{GoogleOAuthServerAuthModule.class.getSimpleName()}); 119 | } 120 | 121 | static AuthException wrapException(final String message, final LoginException loginException) { 122 | LOGGER.log(Level.FINE, "wrapException({0},{1})", new Object[]{message, loginException}); 123 | final AuthException authException = new AuthException(message); 124 | authException.initCause(loginException); 125 | return authException; 126 | } 127 | 128 | /** 129 | * Creates a LoginContext. If No LoginModules configured for loginContextName, null is returned. 130 | * 131 | * @param loginContextName name of the LoginContext to use 132 | * @param googleOAuthCallbackHandler handler to pass to loginContext 133 | * @return LoginContext for loginContextName or null 134 | * @throws AuthException thrown when LoginException is thrown during LoginContext creation 135 | */ 136 | LoginContext createLoginContext(final String loginContextName, final GoogleOAuthCallbackHandler googleOAuthCallbackHandler) throws AuthException { 137 | try { 138 | final LoginContext createdLoginContext = 139 | new LoginContext(loginContextName, googleOAuthCallbackHandler); 140 | return createdLoginContext; 141 | } catch (LoginException ex) { 142 | if (ignoreMissingLoginContext && ex.getMessage().contains("No LoginModules configured")) { 143 | return null; 144 | } else { 145 | final String message = "Unable to create LoginContext"; 146 | LOGGER.log(Level.SEVERE, message, ex); 147 | throw wrapException(message, ex); 148 | } 149 | } catch (SecurityException ex) { 150 | LOGGER.log(Level.SEVERE, "Something very bad happened!", ex); 151 | throw ex; 152 | } 153 | } 154 | 155 | @Override 156 | public Class[] getSupportedMessageTypes() { 157 | return SUPPORTED_MESSAGE_TYPES; 158 | } 159 | 160 | @Override 161 | public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException { 162 | LOGGER.log(Level.FINER, "validateRequest({0}, {1}, {2})", new Object[]{messageInfo, clientSubject, serviceSubject}); 163 | 164 | final HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage(); 165 | final HttpServletResponse response = (HttpServletResponse) messageInfo.getResponseMessage(); 166 | 167 | if (isOauthResponse(request)) { 168 | return handleOauthResponse(messageInfo, request, response, clientSubject); 169 | } else if (isMandatory(messageInfo)) { 170 | return handleMandatoryRequest(messageInfo, request, response, clientSubject); 171 | } else { 172 | return AuthStatus.SUCCESS; 173 | } 174 | } 175 | 176 | AuthStatus handleOauthResponse(final MessageInfo messageInfo, final HttpServletRequest request, final HttpServletResponse response, final Subject clientSubject) throws AuthException { 177 | final String authorizationCode = request.getParameter(GoogleApiUtils.TOKEN_API_CODE_PARAMETER); 178 | final String error = request.getParameter(GoogleApiUtils.TOKEN_API_ERROR_PARAMETER); 179 | if (error != null && !error.isEmpty()) { 180 | LOGGER.log(Level.WARNING, "Error authorizing: {0}", new Object[]{error}); 181 | //FIXME add an error page configuration and return SEND_FAILURE (how do you use FAILURE? it returns blank page) 182 | return AuthStatus.FAILURE; 183 | } else { 184 | final String redirectUri = buildRedirectUri(request); 185 | final AccessTokenInfo accessTokenInfo = GoogleApiUtils.lookupAccessTokenInfo(redirectUri, authorizationCode, clientid, clientSecret); 186 | LOGGER.log(Level.FINE, "Access Token: {0}", new Object[]{accessTokenInfo}); 187 | 188 | final GoogleUserInfo googleUserInfo = GoogleApiUtils.retrieveGoogleUserInfo(accessTokenInfo); 189 | if (googleUserInfo == null) { 190 | //FIXME handle failure better 191 | return AuthStatus.SEND_FAILURE; 192 | } else { 193 | authenticate(messageInfo, request, response, clientSubject, googleUserInfo); 194 | return AuthStatus.SEND_CONTINUE; 195 | } 196 | } 197 | } 198 | 199 | void authenticate(final MessageInfo messageInfo, final HttpServletRequest request, final HttpServletResponse response, final Subject subject, final GoogleUserInfo googleUserInfo) throws AuthException { 200 | final StateHelper stateHelper = new StateHelper(request); 201 | 202 | googleOAuthCallbackHandler.setGoogleUserInfo(googleUserInfo); 203 | 204 | final Subject lcSubject = loginWithLoginContext(); 205 | 206 | LOGGER.log(Level.FINE, "Subject from Login Context: {0}", lcSubject); 207 | 208 | final List groups = buildGroupNames(googleUserInfo, lcSubject.getPrincipals()); 209 | 210 | setCallerPrincipal(subject, googleUserInfo, groups); 211 | messageInfo.getMap().put(AUTH_TYPE_INFO_KEY, AUTH_TYPE_GOOGLE_OAUTH_KEY); 212 | stateHelper.saveSubject(subject); 213 | 214 | final URI orignalRequestUri = stateHelper.extractOriginalRequestPath(); 215 | if (orignalRequestUri != null) { 216 | try { 217 | LOGGER.log(Level.FINE, "redirecting to original request path: {0}", orignalRequestUri); 218 | response.sendRedirect(orignalRequestUri.toString()); 219 | } catch (IOException ex) { 220 | throw new IllegalStateException("Unable to redirect to " + orignalRequestUri, ex); 221 | } 222 | 223 | } 224 | } 225 | 226 | /** 227 | * Calls login with the loginContext and the retrieves the subject. 228 | * 229 | * @return subject of a loginContext after login 230 | * @throws AuthException wrapped LoginException from loginContext.login() 231 | */ 232 | Subject loginWithLoginContext() throws AuthException { 233 | 234 | try { 235 | loginContextWrapper.login(); 236 | return loginContextWrapper.getSubject(); 237 | } catch (LoginException ex) { 238 | throw wrapException("Unable to login with LoginContext", ex); 239 | } 240 | } 241 | 242 | AuthStatus handleMandatoryRequest(final MessageInfo messageInfo, final HttpServletRequest request, final HttpServletResponse response, final Subject clientSubject) throws AuthException { 243 | final StateHelper stateHelper = new StateHelper(request); 244 | 245 | final Subject savedSubject = stateHelper.retrieveSavedSubject(); 246 | if (savedSubject != null) { 247 | LOGGER.log(Level.FINE, "Applying saved subject: {0}", savedSubject); 248 | applySubject(savedSubject, clientSubject); 249 | return AuthStatus.SUCCESS; 250 | } else { 251 | stateHelper.saveOriginalRequestPath(); 252 | final String redirectUri = buildRedirectUri(request); 253 | final URI oauthUri = GoogleApiUtils.buildOauthUri(redirectUri, endpoint, clientid); 254 | try { 255 | LOGGER.log(Level.FINE, "redirecting to {0} for OAuth", new Object[]{oauthUri}); 256 | response.sendRedirect(oauthUri.toString()); 257 | } catch (IOException ex) { 258 | throw new IllegalStateException("Unable to redirect to " + oauthUri, ex); 259 | } 260 | return AuthStatus.SEND_CONTINUE; 261 | } 262 | } 263 | 264 | /** 265 | * Builds a list of group names which contain any groups from defaultGroups and any principals from LoginContext 266 | * 267 | * @param googleUserInfo user being authenticate, the domain of the email may be used for a group 268 | * @param principals principals from LoginContext 269 | * @return list of groupNames for the user 270 | */ 271 | List buildGroupNames(final GoogleUserInfo googleUserInfo, final Iterable principals) { 272 | final List groups = new ArrayList(); 273 | 274 | // add default groups if defined 275 | if (!defaultGroups.isEmpty()) { 276 | groups.addAll(Arrays.asList(defaultGroups.split(","))); 277 | } 278 | 279 | // add domain of email as group 280 | if (addDomainAsGroup && googleUserInfo.getEmail().contains("@")) { 281 | final String domain = googleUserInfo.getEmail().split("@", 2)[1]; 282 | groups.add(domain); 283 | } 284 | 285 | //add each principal as a group 286 | for (final Principal principal : principals) { 287 | groups.add(principal.getName()); 288 | } 289 | 290 | return groups; 291 | } 292 | 293 | boolean isOauthResponse(final HttpServletRequest request) { 294 | return request.getRequestURI().contains(oauthAuthenticationCallbackUri);//FIXME needs better check 295 | } 296 | 297 | String buildRedirectUri(final HttpServletRequest request) { 298 | final String serverScheme = request.getScheme(); 299 | final String serverUserInfo = null; 300 | final String serverHost = request.getServerName(); 301 | final int serverPort = request.getServerPort(); 302 | final String path = request.getContextPath() + oauthAuthenticationCallbackUri; 303 | final String query = null; 304 | final String serverFragment = null; 305 | try { 306 | return new URI(serverScheme, serverUserInfo, serverHost, serverPort, path, query, serverFragment).toString(); 307 | } catch (URISyntaxException ex) { 308 | throw new IllegalStateException("Unable to build redirectUri", ex); 309 | } 310 | } 311 | 312 | boolean setCallerPrincipal(Subject clientSubject, GoogleUserInfo googleUserInfo, List groups) { 313 | final CallerPrincipalCallback principalCallback = new CallerPrincipalCallback( 314 | clientSubject, new GoogleOAuthPrincipal(googleUserInfo)); 315 | 316 | final Callback[] callbacks; 317 | if (groups.isEmpty()) { 318 | callbacks = new Callback[]{principalCallback}; 319 | } else { 320 | final GroupPrincipalCallback groupCallback = new GroupPrincipalCallback(clientSubject, groups.toArray(new String[0])); 321 | callbacks = new Callback[]{principalCallback, groupCallback}; 322 | } 323 | 324 | try { 325 | handler.handle(callbacks); 326 | } catch (Exception e) { 327 | LOGGER.log(Level.SEVERE, "unable to set caller and groups", e); 328 | return false; 329 | } 330 | 331 | return true; 332 | } 333 | 334 | static void applySubject(final Subject source, Subject destination) { 335 | destination.getPrincipals().addAll( 336 | source.getPrincipals()); 337 | destination.getPublicCredentials().addAll(source.getPublicCredentials()); 338 | destination.getPrivateCredentials().addAll(source.getPrivateCredentials()); 339 | } 340 | 341 | static boolean isMandatory(MessageInfo messageInfo) { 342 | return Boolean.valueOf((String) messageInfo.getMap().get(IS_MANDATORY_INFO_KEY)); 343 | } 344 | 345 | @Override 346 | public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException { 347 | LOGGER.log(Level.FINER, "secureResponse()"); 348 | return AuthStatus.SEND_SUCCESS; 349 | } 350 | 351 | @Override 352 | public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException { 353 | subject.getPrincipals().clear(); 354 | subject.getPublicCredentials().clear(); 355 | subject.getPrivateCredentials().clear(); 356 | 357 | try { 358 | loginContextWrapper.logout(); 359 | } catch (LoginException ex) { 360 | throw wrapException("Unable to logout LoginContext", ex); 361 | } 362 | 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/GoogleUserInfoCallBack.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google; 2 | 3 | import com.idmworks.security.google.api.GoogleUserInfo; 4 | import java.io.Serializable; 5 | import javax.security.auth.callback.Callback; 6 | 7 | /** 8 | * Callback for GoogleUserInfo. 9 | * 10 | * @author pdgreen 11 | */ 12 | public class GoogleUserInfoCallBack implements Callback, Serializable { 13 | 14 | private GoogleUserInfo googleUserInfo; 15 | 16 | public GoogleUserInfo getGoogleUserInfo() { 17 | return googleUserInfo; 18 | } 19 | 20 | void setGoogleUserInfo(GoogleUserInfo googleUserInfo) { 21 | this.googleUserInfo = googleUserInfo; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/LoginContextWrapper.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google; 2 | 3 | import javax.security.auth.Subject; 4 | import javax.security.auth.login.LoginContext; 5 | import javax.security.auth.login.LoginException; 6 | 7 | /** 8 | * Wrapper for {@link LoginContext} which properly handles null. 9 | * 10 | * @author pdgreen 11 | */ 12 | public class LoginContextWrapper { 13 | 14 | private final LoginContext wrapped; 15 | 16 | public LoginContextWrapper(LoginContext wrapped) { 17 | this.wrapped = wrapped; 18 | } 19 | 20 | public void login() throws LoginException { 21 | if (wrapped != null) { 22 | wrapped.login(); 23 | } 24 | } 25 | 26 | public void logout() throws LoginException { 27 | if (wrapped != null) { 28 | wrapped.logout(); 29 | } 30 | } 31 | 32 | public Subject getSubject() { 33 | if (wrapped != null) { 34 | return wrapped.getSubject(); 35 | } else { 36 | return new Subject(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/ParseUtils.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google; 2 | 3 | import com.idmworks.security.google.api.GoogleUserInfo; 4 | import java.util.Date; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import static com.idmworks.security.google.GoogleApiUtils.*; 9 | 10 | /** 11 | * Methods of this class parse JSON responses.
Instead of including an external library to handle JSON, and making 12 | * installation/packaging more difficult, I created a couple small methods to handle it. There is only a couple response 13 | * to parse, so I felt I could by pass a full JSON parse for the sake of easier installation. 14 | * 15 | * @author pdgreen 16 | */ 17 | public class ParseUtils { 18 | 19 | static Map parseSimpleJson(final String json) { 20 | final String[] parts = json.substring(json.indexOf("{") + 1, json.lastIndexOf("}")).split(","); 21 | 22 | final Map values = new HashMap(); 23 | for (final String part : parts) { 24 | final String[] vparts = part.replaceAll("\"", "").split(":", 2); 25 | values.put(vparts[0].trim(), vparts[1].trim()); 26 | } 27 | return values; 28 | } 29 | 30 | public static AccessTokenInfo parseAccessTokenJson(final String json) { 31 | 32 | final Map values = parseSimpleJson(json); 33 | 34 | final String accessToken = values.get(TOKEN_API_ACCESS_TOKEN_PARAMETER); 35 | final String expiresInAsString = values.get(TOKEN_API_EXPIRES_IN_PARAMETER); 36 | final String tokenType = values.get(TOKEN_API_TOKEN_TYPE_PARAMETER); 37 | 38 | final int expiresIn = Integer.parseInt(expiresInAsString); 39 | 40 | return new AccessTokenInfo(accessToken, new Date(new Date().getTime() + expiresIn * 1000), tokenType); 41 | } 42 | 43 | public static GoogleUserInfo parseGoogleUserInfoJson(final String json) { 44 | 45 | final Map values = parseSimpleJson(json); 46 | 47 | final String id = values.get(USERINFO_API_ID_PARAMETER); 48 | final String email = values.get(USERINFO_API_EMAIL_PARAMETER); 49 | final boolean verifiedEmail = values.containsKey(USERINFO_API_VERIFIED_EMAIL_PARAMETER) && Boolean.parseBoolean(values.get(USERINFO_API_VERIFIED_EMAIL_PARAMETER)); 50 | final String name = values.get(USERINFO_API_NAME_PARAMETER); 51 | final String givenName = values.get(USERINFO_API_GIVEN_NAME_PARAMETER); 52 | final String familyName = values.get(USERINFO_API_FAMILY_NAME_PARAMETER); 53 | final String gender = values.get(USERINFO_API_GENDER_PARAMETER); 54 | final String link = values.get(USERINFO_API_LINK_PARAMETER); 55 | final String picture = values.get(USERINFO_API_PICTURE_PARAMETER); 56 | final String locale = values.get(USERINFO_API_LOCALE_PARAMETER); 57 | 58 | return new GoogleUserInfo(id, email, verifiedEmail, name, givenName, familyName, gender, link, picture, locale); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/StateHelper.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google; 2 | 3 | import java.net.URI; 4 | import java.net.URISyntaxException; 5 | import java.util.logging.Level; 6 | import java.util.logging.Logger; 7 | import javax.security.auth.Subject; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpSession; 10 | 11 | /** 12 | * Provides methods for saving and retrieving state. 13 | * 14 | * @author pdgreen 15 | */ 16 | public class StateHelper { 17 | 18 | private static Logger LOGGER = Logger.getLogger(GoogleOAuthServerAuthModule.class.getName()); 19 | /* 20 | * Session Parameters 21 | */ 22 | private static final String SESSION_PREFIX = StateHelper.class.getName() + "."; 23 | private static final String ORIGINAL_REQUEST_PATH = SESSION_PREFIX + "original_request_path"; 24 | private static final String SAVED_SUBJECT = SESSION_PREFIX + "saved_subject"; 25 | private final HttpServletRequest request; 26 | 27 | public StateHelper(HttpServletRequest request) { 28 | this.request = request; 29 | } 30 | 31 | public void saveSubject(final Subject subject) { 32 | if (subject == null) { 33 | return; 34 | } 35 | final HttpSession session = request.getSession(true); 36 | 37 | session.setAttribute(SAVED_SUBJECT, subject); 38 | LOGGER.log(Level.FINE, "Saved subject {0}", subject); 39 | } 40 | 41 | public Subject retrieveSavedSubject() { 42 | final HttpSession session = request.getSession(false); 43 | if (session != null) { 44 | return (Subject) session.getAttribute(SAVED_SUBJECT); 45 | } else { 46 | return null; 47 | } 48 | } 49 | 50 | public void saveOriginalRequestPath() { 51 | final HttpSession session = request.getSession(true); 52 | try { 53 | final URI orignalRequestUri = new URI(request.getRequestURI()); 54 | session.setAttribute(ORIGINAL_REQUEST_PATH, orignalRequestUri); 55 | LOGGER.log(Level.FINE, "Saved original request path {0}", orignalRequestUri); 56 | } catch (URISyntaxException ex) { 57 | LOGGER.log(Level.WARNING, "Unable to save original request path", ex); 58 | } 59 | } 60 | 61 | public URI extractOriginalRequestPath() { 62 | final HttpSession session = request.getSession(false); 63 | if (session != null) { 64 | final URI originalRequestPath = (URI) session.getAttribute(ORIGINAL_REQUEST_PATH); 65 | session.removeAttribute(ORIGINAL_REQUEST_PATH); 66 | return originalRequestPath; 67 | } else { 68 | return null; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/api/GoogleOAuthPrincipal.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google.api; 2 | 3 | import java.security.Principal; 4 | 5 | /** 6 | * Principal for user authenticated with Google OAuth. 7 | * 8 | * @author pdgreen 9 | */ 10 | public class GoogleOAuthPrincipal implements Principal { 11 | 12 | private final GoogleUserInfo googleUserInfo; 13 | 14 | public GoogleOAuthPrincipal(GoogleUserInfo googleUserInfo) { 15 | this.googleUserInfo = googleUserInfo; 16 | } 17 | 18 | @Override 19 | public String getName() { 20 | return googleUserInfo.getEmail(); 21 | } 22 | 23 | public GoogleUserInfo getGoogleUserInfo() { 24 | return googleUserInfo; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return new StringBuilder().append("{").append(GoogleOAuthPrincipal.class.getSimpleName()).append(":").append(getName()).append("}").toString(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/idmworks/security/google/api/GoogleUserInfo.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google.api; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * User information from google account. 7 | * 8 | * @author pdgreen 9 | */ 10 | public class GoogleUserInfo implements Serializable { 11 | 12 | private final String id; 13 | private final String email; 14 | private final boolean verifiedEmail; 15 | private final String name; 16 | private final String givenName; 17 | private final String familyName; 18 | private final String gender; 19 | private final String link; 20 | private final String picture; 21 | private final String locale; 22 | 23 | public GoogleUserInfo(String id, String email, boolean verifiedEmail, String name, String givenName, String familyName, String gender, String link, String picture, String locale) { 24 | this.id = id; 25 | this.email = email; 26 | this.verifiedEmail = verifiedEmail; 27 | this.name = name; 28 | this.givenName = givenName; 29 | this.familyName = familyName; 30 | this.gender = gender; 31 | this.link = link; 32 | this.picture = picture; 33 | this.locale = locale; 34 | } 35 | 36 | public String getEmail() { 37 | return email; 38 | } 39 | 40 | public String getFamilyName() { 41 | return familyName; 42 | } 43 | 44 | public String getGender() { 45 | return gender; 46 | } 47 | 48 | public String getGivenName() { 49 | return givenName; 50 | } 51 | 52 | public String getId() { 53 | return id; 54 | } 55 | 56 | public String getLink() { 57 | return link; 58 | } 59 | 60 | public String getLocale() { 61 | return locale; 62 | } 63 | 64 | public String getName() { 65 | return name; 66 | } 67 | 68 | public String getPicture() { 69 | return picture; 70 | } 71 | 72 | public boolean isVerifiedEmail() { 73 | return verifiedEmail; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/com/idmworks/security/google/ParseUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.idmworks.security.google; 2 | 3 | import com.idmworks.security.google.api.GoogleUserInfo; 4 | import org.junit.*; 5 | import static org.junit.Assert.*; 6 | import static org.hamcrest.CoreMatchers.*; 7 | 8 | /** 9 | * Tests for {@link ParseUtils}. 10 | * 11 | * @author pdgreen 12 | */ 13 | public class ParseUtilsTest { 14 | 15 | @Test 16 | public void testParseAccessTokenJson() { 17 | String json = 18 | "{\n" 19 | + "\"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\n" 20 | + "\"expires_in\":3920,\n" 21 | + "\"token_type\":\"Bearer\"\n" 22 | + "}"; 23 | 24 | final AccessTokenInfo result = ParseUtils.parseAccessTokenJson(json); 25 | 26 | assertThat(result, is(notNullValue())); 27 | assertThat(result.getAccessToken(), is("1/fFAGRNJru1FTz70BzhT3Zg")); 28 | assertThat(result.getType(), is("Bearer")); 29 | } 30 | 31 | @Test 32 | public void testParseGoogleUserInfoJson() { 33 | String json = 34 | "{\n" 35 | + "\"id\": \"1074968992519869407200\",\n" 36 | + "\"email\": \"fake.name@gmail.com\",\n" 37 | + "\"verified_email\": true,\n" 38 | + "\"name\": \"Fake Name\",\n" 39 | + "\"given_name\": \"Fake\",\n" 40 | + "\"family_name\": \"Name\",\n" 41 | + "\"link\": \"https://plus.google.com/1074968992519869407200\",\n" 42 | + "\"picture\": \"https://lh4.googleusercontent.com/path/to/photo.jpg\",\n" 43 | + "\"gender\": \"other\",\n" 44 | + "\"locale\": \"en-US\"\n" 45 | + "}"; 46 | 47 | final GoogleUserInfo result = ParseUtils.parseGoogleUserInfoJson(json); 48 | 49 | assertThat(result, is(notNullValue())); 50 | assertThat(result.getId(), is("1074968992519869407200")); 51 | assertThat(result.getEmail(), is("fake.name@gmail.com")); 52 | assertThat(result.isVerifiedEmail(), is(true)); 53 | assertThat(result.getName(), is("Fake Name")); 54 | assertThat(result.getGivenName(), is("Fake")); 55 | assertThat(result.getFamilyName(), is("Name")); 56 | assertThat(result.getGender(), is("other")); 57 | assertThat(result.getLink(), is("https://plus.google.com/1074968992519869407200")); 58 | assertThat(result.getPicture(), is("https://lh4.googleusercontent.com/path/to/photo.jpg")); 59 | assertThat(result.getLocale(), is("en-US")); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/resources/test.config: -------------------------------------------------------------------------------- 1 | test-DomainLoginModule { 2 | com.idmworks.security.google.DomainLoginModule required; 3 | }; --------------------------------------------------------------------------------