-
Notifications
You must be signed in to change notification settings - Fork 70
/
OAuthAuthenticator.java
351 lines (325 loc) · 12 KB
/
OAuthAuthenticator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.security.oauth;
import static org.eclipse.che.dto.server.DtoFactory.newDto;
import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
import com.google.api.client.auth.oauth2.AuthorizationCodeResponseUrl;
import com.google.api.client.auth.oauth2.BearerToken;
import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.MemoryDataStoreFactory;
import jakarta.ws.rs.core.MediaType;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
import org.eclipse.che.commons.json.JsonHelper;
import org.eclipse.che.commons.json.JsonParseException;
import org.eclipse.che.security.oauth.shared.OAuthTokenProvider;
import org.eclipse.che.security.oauth.shared.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Authentication service which allow get access token from OAuth provider site. */
public abstract class OAuthAuthenticator {
private static final String AUTHENTICATOR_IS_NOT_CONFIGURED = "Authenticator is not configured";
private static final Logger LOG = LoggerFactory.getLogger(OAuthAuthenticator.class);
protected AuthorizationCodeFlow flow;
protected Map<Pattern, String> redirectUrisMap;
/**
* @see {@link #configure(String, String, String[], String, String, MemoryDataStoreFactory, List)}
*/
protected void configure(
String clientId,
String clientSecret,
String[] redirectUris,
String authUri,
String tokenUri,
MemoryDataStoreFactory dataStoreFactory)
throws IOException {
configure(
clientId,
clientSecret,
redirectUris,
authUri,
tokenUri,
dataStoreFactory,
Collections.emptyList());
}
/**
* This method should be invoked by child class for initialization default instance of {@link
* AuthorizationCodeFlow} that will be used for authorization
*/
protected void configure(
String clientId,
String clientSecret,
String[] redirectUris,
String authUri,
String tokenUri,
MemoryDataStoreFactory dataStoreFactory,
List<String> scopes)
throws IOException {
final AuthorizationCodeFlow authorizationFlow =
new AuthorizationCodeFlow.Builder(
BearerToken.authorizationHeaderAccessMethod(),
new NetHttpTransport(),
new JacksonFactory(),
new GenericUrl(tokenUri),
new ClientParametersAuthentication(clientId, clientSecret),
clientId,
authUri)
.setDataStoreFactory(dataStoreFactory)
.setScopes(scopes)
.build();
LOG.debug(
"clientId={}, clientSecret={}, redirectUris={} , authUri={}, tokenUri={}, dataStoreFactory={}",
clientId,
clientSecret,
redirectUris,
authUri,
tokenUri,
dataStoreFactory);
configure(authorizationFlow, Arrays.asList(redirectUris));
}
/**
* This method should be invoked by child class for setting instance of {@link
* AuthorizationCodeFlow} that will be used for authorization
*/
protected void configure(AuthorizationCodeFlow flow, List<String> redirectUris) {
this.flow = flow;
this.redirectUrisMap = new HashMap<>(redirectUris.size());
for (String uri : redirectUris) {
// Redirect URI may be in form urn:ietf:wg:oauth:2.0:oob os use java.net.URI instead of
// java.net.URL
this.redirectUrisMap.put(
Pattern.compile("([a-z0-9\\-]+\\.)?" + URI.create(uri).getHost()), uri);
}
}
/**
* Create authentication URL.
*
* @param requestUrl URL of current HTTP request. This parameter required to be able determine URL
* for redirection after authentication. If URL contains query parameters they will be copy to
* 'state' parameter and returned to callback method.
* @param scopes specify exactly what type of access needed
* @return URL for authentication
*/
public String getAuthenticateUrl(URL requestUrl, List<String> scopes)
throws OAuthAuthenticationException {
if (!isConfigured()) {
throw new OAuthAuthenticationException(AUTHENTICATOR_IS_NOT_CONFIGURED);
}
AuthorizationCodeRequestUrl url =
flow.newAuthorizationUrl().setRedirectUri(findRedirectUrl(requestUrl)).setScopes(scopes);
url.setState(prepareState(requestUrl));
return url.build();
}
protected String prepareState(URL requestUrl) {
StringBuilder state = new StringBuilder();
String query = requestUrl.getQuery();
if (query != null) {
if (state.length() > 0) {
state.append('&');
}
state.append(query);
}
return state.toString();
}
protected String findRedirectUrl(URL requestUrl) {
final String requestHost = requestUrl.getHost();
for (Map.Entry<Pattern, String> e : redirectUrisMap.entrySet()) {
if (e.getKey().matcher(requestHost).matches()) {
return e.getValue();
}
}
return null; // TODO : throw exception instead of return null ???
}
/**
* Process callback request.
*
* @param requestUrl request URI. URI should contain authorization code generated by authorization
* server
* @param scopes specify exactly what type of access needed. This list must be exactly the same as
* list passed to the method {@link #getAuthenticateUrl(URL, java.util.List)}
* @return id of authenticated user
* @throws OAuthAuthenticationException if authentication failed or <code>requestUrl</code> does
* not contain required parameters, e.g. 'code'
*/
public String callback(URL requestUrl, List<String> scopes) throws OAuthAuthenticationException {
if (!isConfigured()) {
throw new OAuthAuthenticationException(AUTHENTICATOR_IS_NOT_CONFIGURED);
}
AuthorizationCodeResponseUrl authorizationCodeResponseUrl =
new AuthorizationCodeResponseUrl(requestUrl.toString());
final String error = authorizationCodeResponseUrl.getError();
if (error != null) {
throw new OAuthAuthenticationException("Authentication failed: " + error);
}
final String code = authorizationCodeResponseUrl.getCode();
if (code == null) {
throw new OAuthAuthenticationException("Missing authorization code. ");
}
try {
TokenResponse tokenResponse =
flow.newTokenRequest(code)
.setRequestInitializer(
request -> {
if (request.getParser() == null) {
request.setParser(flow.getJsonFactory().createJsonObjectParser());
}
request.getHeaders().setAccept(MediaType.APPLICATION_JSON);
})
.setRedirectUri(findRedirectUrl(requestUrl))
.setScopes(scopes)
.execute();
String userId = getUserFromUrl(authorizationCodeResponseUrl);
if (userId == null) {
userId =
getUser(newDto(OAuthToken.class).withToken(tokenResponse.getAccessToken())).getId();
}
flow.createAndStoreCredential(tokenResponse, userId);
return userId;
} catch (IOException ioe) {
throw new OAuthAuthenticationException(ioe.getMessage());
}
}
/**
* Get user info.
*
* @param accessToken oauth access token
* @return user info
* @throws OAuthAuthenticationException if fail to get user info
*/
public abstract User getUser(OAuthToken accessToken) throws OAuthAuthenticationException;
/**
* Get the name of OAuth provider supported by current implementation.
*
* @return oauth provider name
*/
public abstract String getOAuthProvider();
private String getUserFromUrl(AuthorizationCodeResponseUrl authorizationCodeResponseUrl)
throws IOException {
String state = authorizationCodeResponseUrl.getState();
if (!(state == null || state.isEmpty())) {
String decoded = URLDecoder.decode(state, "UTF-8");
String[] items = decoded.split("&");
for (String str : items) {
if (str.startsWith("userId=")) {
return str.substring(7, str.length());
}
}
}
return null;
}
protected <O> O getJson(String getUserUrl, Class<O> userClass)
throws OAuthAuthenticationException {
HttpURLConnection urlConnection = null;
InputStream urlInputStream = null;
try {
urlConnection = (HttpURLConnection) new URL(getUserUrl).openConnection();
urlInputStream = urlConnection.getInputStream();
return JsonHelper.fromJson(urlInputStream, userClass, null);
} catch (JsonParseException | IOException e) {
throw new OAuthAuthenticationException(e.getMessage(), e);
} finally {
if (urlInputStream != null) {
try {
urlInputStream.close();
} catch (IOException ignored) {
}
}
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
/**
* Return authorization token by userId.
*
* <p>WARN!!!. DO not use it directly.
*
* @param userId user identifier
* @return token value or {@code null}. When user have valid token then it will be returned, when
* user have expired token and it can be refreshed then refreshed value will be returned, when
* none token found for user then {@code null} will be returned, when user have expired token
* and it can't be refreshed then {@code null} will be returned
* @throws IOException when error occurs during token loading
* @see OAuthTokenProvider#getToken(String, String)
*/
public OAuthToken getToken(String userId) throws IOException {
if (!isConfigured()) {
throw new IOException(AUTHENTICATOR_IS_NOT_CONFIGURED);
}
Credential credential = flow.loadCredential(userId);
if (credential == null) {
return null;
}
final Long expirationTime = credential.getExpiresInSeconds();
if (expirationTime != null && expirationTime < 0) {
boolean tokenRefreshed;
try {
tokenRefreshed = credential.refreshToken();
} catch (IOException ioEx) {
tokenRefreshed = false;
}
if (tokenRefreshed) {
credential = flow.loadCredential(userId);
} else {
// if token is not refreshed then old value should be invalidated
// and null result should be returned
try {
invalidateToken(userId);
} catch (IOException ignored) {
}
return null;
}
}
return newDto(OAuthToken.class).withToken(credential.getAccessToken());
}
/**
* Invalidate OAuth token for specified user.
*
* @param userId user
* @return <code>true</code> if OAuth token invalidated and <code>false</code> otherwise, e.g. if
* user does not have token yet
*/
public boolean invalidateToken(String userId) throws IOException {
Credential credential = flow.loadCredential(userId);
if (credential != null) {
flow.getCredentialDataStore().delete(userId);
return true;
}
return false;
}
/**
* Checks configuring of authenticator
*
* @return true only if authenticator have valid configuration data and it is able to authorize
* otherwise returns false
*/
public boolean isConfigured() {
return flow != null;
}
}