├── .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 | [](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 | };
--------------------------------------------------------------------------------