• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

smartsheet / smartsheet-java-sdk / #55

02 Oct 2024 07:40PM UTC coverage: 60.548% (+0.7%) from 59.836%
#55

push

github

web-flow
Release prep for 3.2.1 (#103)

4156 of 6864 relevant lines covered (60.55%)

0.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.33
/src/main/java/com/smartsheet/api/internal/oauth/OAuthFlowImpl.java
1
/*
2
 * Copyright (C) 2024 Smartsheet
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package com.smartsheet.api.internal.oauth;
18

19
import com.smartsheet.api.InvalidRequestException;
20
import com.smartsheet.api.internal.http.HttpClient;
21
import com.smartsheet.api.internal.http.HttpClientException;
22
import com.smartsheet.api.internal.http.HttpMethod;
23
import com.smartsheet.api.internal.http.HttpRequest;
24
import com.smartsheet.api.internal.http.HttpResponse;
25
import com.smartsheet.api.internal.json.JSONSerializerException;
26
import com.smartsheet.api.internal.json.JsonSerializer;
27
import com.smartsheet.api.internal.util.QueryUtil;
28
import com.smartsheet.api.internal.util.Util;
29
import com.smartsheet.api.oauth.AccessDeniedException;
30
import com.smartsheet.api.oauth.AccessScope;
31
import com.smartsheet.api.oauth.AuthorizationResult;
32
import com.smartsheet.api.oauth.InvalidOAuthClientException;
33
import com.smartsheet.api.oauth.InvalidOAuthGrantException;
34
import com.smartsheet.api.oauth.InvalidScopeException;
35
import com.smartsheet.api.oauth.InvalidTokenRequestException;
36
import com.smartsheet.api.oauth.OAuthAuthorizationCodeException;
37
import com.smartsheet.api.oauth.OAuthFlow;
38
import com.smartsheet.api.oauth.OAuthTokenException;
39
import com.smartsheet.api.oauth.Token;
40
import com.smartsheet.api.oauth.UnsupportedOAuthGrantTypeException;
41
import com.smartsheet.api.oauth.UnsupportedResponseTypeException;
42

43
import java.io.InputStream;
44
import java.net.URI;
45
import java.net.URISyntaxException;
46
import java.nio.charset.StandardCharsets;
47
import java.security.MessageDigest;
48
import java.security.NoSuchAlgorithmException;
49
import java.util.EnumSet;
50
import java.util.HashMap;
51
import java.util.Map;
52

53
/**
54
 * Default implementation of OAuthFlow.
55
 * <p>
56
 * Thread Safety: Implementation of this interface must be thread safe.
57
 */
58
public class OAuthFlowImpl implements OAuthFlow {
59
    /**
60
     * Represents the HttpClient.
61
     * <p>
62
     * It will be initialized in constructor and will not change afterwards.
63
     */
64
    private HttpClient httpClient;
65

66
    /**
67
     * Represents the JsonSerializer.
68
     * <p>
69
     * It will be initialized in constructor and will not change afterwards.
70
     */
71
    private JsonSerializer jsonSerializer;
72

73
    /**
74
     * Represents the Client ID.
75
     * <p>
76
     * It will be initialized in constructor and will not change afterwards.
77
     */
78
    private String clientId;
79

80
    /**
81
     * Represents the Client Secret.
82
     * <p>
83
     * It will be initialized in constructor and will not change afterwards.
84
     */
85
    private String clientSecret;
86

87
    /**
88
     * Represents the redirect URL.
89
     * <p>
90
     * It will be initialized in constructor and will not change afterwards.
91
     */
92
    private String redirectURL;
93

94
    /**
95
     * Represents the authorization URL.
96
     * <p>
97
     * It will be initialized in constructor and will not change afterwards.
98
     */
99
    private String authorizationURL;
100

101
    /**
102
     * Represents the token URL.
103
     * <p>
104
     * It will be initialized in constructor and will not change afterwards.
105
     */
106
    private String tokenURL;
107

108
    private static final String CODE = "code";
109
    private static final String CLIENT_ID = "client_id";
110
    private static final String REDIRECT_URI = "redirect_uri";
111
    private static final String ERROR = "error";
112
    private static final String REFRESH_TOKEN = "refresh_token";
113

114
    /**
115
     * Constructor.
116
     * <p>
117
     *
118
     * @param clientId         the client id
119
     * @param clientSecret     the client secret
120
     * @param redirectURL      the redirect url
121
     * @param authorizationURL the authorization url
122
     * @param tokenURL         the token url
123
     * @param httpClient       the http client
124
     * @param jsonSerializer   the json serializer
125
     * @throws IllegalArgumentException If any argument is null, or empty string.
126
     */
127
    public OAuthFlowImpl(
128
            String clientId,
129
            String clientSecret,
130
            String redirectURL,
131
            String authorizationURL,
132
            String tokenURL,
133
            HttpClient httpClient,
134
            JsonSerializer jsonSerializer
135
    ) {
1✔
136
        Util.throwIfNull(clientId, clientSecret, redirectURL, authorizationURL, tokenURL, httpClient, jsonSerializer);
1✔
137
        Util.throwIfEmpty(clientId, clientSecret, redirectURL, authorizationURL, tokenURL);
1✔
138

139
        this.clientId = clientId;
1✔
140
        this.clientSecret = clientSecret;
1✔
141
        this.redirectURL = redirectURL;
1✔
142
        this.authorizationURL = authorizationURL;
1✔
143
        this.tokenURL = tokenURL;
1✔
144
        this.httpClient = httpClient;
1✔
145
        this.jsonSerializer = jsonSerializer;
1✔
146
    }
1✔
147

148
    /**
149
     * Generate a new authorization URL.
150
     * <p>
151
     * Exceptions: - IllegalArgumentException : if scopes is null/empty
152
     *
153
     * @param scopes the scopes
154
     * @param state  an arbitrary string that will be returned to your app; intended to be used by you to ensure that
155
     *               this redirect is indeed from an OAuth flow that you initiated
156
     * @return the authorization URL
157
     */
158
    public String newAuthorizationURL(EnumSet<AccessScope> scopes, String state) {
159
        Util.throwIfNull(scopes);
1✔
160
        if (state == null) {
1✔
161
            state = "";
1✔
162
        }
163

164
        // Build a map of parameters for the URL
165
        Map<String, Object> params = new HashMap<>();
1✔
166
        params.put("response_type", CODE);
1✔
167
        params.put(CLIENT_ID, clientId);
1✔
168
        params.put(REDIRECT_URI, redirectURL);
1✔
169
        params.put("state", state);
1✔
170

171
        StringBuilder scopeBuffer = new StringBuilder();
1✔
172
        for (AccessScope scope : scopes) {
1✔
173
            scopeBuffer.append(scope.name() + ",");
1✔
174
        }
1✔
175
        params.put("scope", scopeBuffer.substring(0, scopeBuffer.length() - 1));
1✔
176

177
        // Generate the URL with the parameters
178
        return QueryUtil.generateUrl(authorizationURL, params);
1✔
179
    }
180

181
    /**
182
     * Extract AuthorizationResult from the authorization response URL (i.e. the redirectURL with the response
183
     * parameters from Smartsheet OAuth server).
184
     *
185
     * @param authorizationResponseURL the authorization response URL
186
     * @return the authorization result
187
     * @throws URISyntaxException               the URI syntax exception
188
     * @throws IllegalArgumentException         if authorizationResponseURL is null/empty, or a malformed URL
189
     * @throws AccessDeniedException            if the user has denied the authorization request
190
     * @throws UnsupportedResponseTypeException if the response type isn't supported
191
     *                                          (note that this won't really happen in current implementation)
192
     * @throws InvalidScopeException            if some of the specified scopes are invalid
193
     * @throws OAuthAuthorizationCodeException  if any other error occurred during the operation
194
     */
195
    public AuthorizationResult extractAuthorizationResult(String authorizationResponseURL)
196
            throws URISyntaxException, OAuthAuthorizationCodeException {
197
        Util.throwIfNull(authorizationResponseURL);
1✔
198
        Util.throwIfEmpty(authorizationResponseURL);
1✔
199

200
        // Get all of the params from the URL
201
        URI uri = new URI(authorizationResponseURL);
1✔
202
        String query = uri.getQuery();
1✔
203
        if (query == null) {
1✔
204
            throw new OAuthAuthorizationCodeException("There must be a query string in the response URL");
1✔
205
        }
206

207
        Map<String, String> map = new HashMap<>();
1✔
208
        for (String param : query.split("&")) {
1✔
209
            int index = param.indexOf('=');
1✔
210
            map.put(param.substring(0, index), param.substring(index + 1));
1✔
211
        }
212

213
        // Check for an error response in the URL and throw it.
214
        String error = map.get(ERROR);
1✔
215
        if (error != null && !error.isEmpty()) {
1✔
216
            if ("access_denied".equals(error)) {
1✔
217
                throw new AccessDeniedException("Access denied.");
1✔
218
            } else if ("unsupported_response_type".equals(error)) {
1✔
219
                throw new UnsupportedResponseTypeException("response_type must be set to \"code\".");
1✔
220
            } else if ("invalid_scope".equals(error)) {
1✔
221
                throw new InvalidScopeException("One or more of the requested access scopes are invalid. " +
1✔
222
                        "Please check the list of access scopes");
223
            } else {
224
                throw new OAuthAuthorizationCodeException("An undefined error was returned of type: " + error);
1✔
225
            }
226
        }
227

228
        AuthorizationResult authorizationResult = new AuthorizationResult();
1✔
229
        authorizationResult.setCode(map.get(CODE));
1✔
230
        authorizationResult.setState(map.get("state"));
1✔
231
        Long expiresIn;
232
        try {
233
            expiresIn = Long.parseLong(map.get("expires_in"));
1✔
234
        } catch (NumberFormatException ex) {
1✔
235
            expiresIn = 0L;
1✔
236
        }
1✔
237
        authorizationResult.setExpiresInSeconds(expiresIn);
1✔
238

239
        return authorizationResult;
1✔
240
    }
241

242
    /**
243
     * Obtain a new token using AuthorizationResult.
244
     * <p>
245
     * Exceptions:
246
     * - IllegalArgumentException : if authorizationResult is null
247
     * - InvalidTokenRequestException : if the token request is invalid (note that this won't really happen in current implementation)
248
     * - InvalidOAuthClientException : if the client information is invalid
249
     * - InvalidOAuthGrantException : if the authorization code or refresh token is invalid or expired, the
250
     * redirect_uri does not match, or the hash value does not match the client secret and/or code
251
     * - UnsupportedOAuthGrantTypeException : if the grant type is invalid (note that this won't really happen in
252
     * current implementation)
253
     * - OAuthTokenException : if any other error occurred during the operation
254
     *
255
     * @param authorizationResult the authorization result
256
     * @return the token
257
     * @throws OAuthTokenException     the o auth token exception
258
     * @throws JSONSerializerException the JSON serializer exception
259
     * @throws HttpClientException     the http client exception
260
     * @throws URISyntaxException      the URI syntax exception
261
     * @throws InvalidRequestException the invalid request exception
262
     */
263
    public Token obtainNewToken(AuthorizationResult authorizationResult)
264
            throws OAuthTokenException, JSONSerializerException, HttpClientException, URISyntaxException, InvalidRequestException {
265
        if (authorizationResult == null) {
1✔
266
            throw new IllegalArgumentException();
×
267
        }
268

269
        // Prepare the hash
270

271
        String doHash = clientSecret + "|" + authorizationResult.getCode();
1✔
272

273
        MessageDigest md;
274
        try {
275
            md = MessageDigest.getInstance("SHA-256");
1✔
276
        } catch (NoSuchAlgorithmException e) {
×
277
            throw new RuntimeException("Your JVM does not support SHA-256, which is required for OAuth with Smartsheet.", e);
×
278
        }
1✔
279
        byte[] digest;
280
        digest = md.digest(doHash.getBytes(StandardCharsets.UTF_8));
1✔
281

282
        String hash = org.apache.commons.codec.binary.Hex.encodeHexString(digest);
1✔
283

284
        // create a Map of the parameters
285
        Map<String, Object> params = new HashMap<>();
1✔
286
        params.put("grant_type", "authorization_code");
1✔
287
        params.put(CLIENT_ID, clientId);
1✔
288
        params.put(CODE, authorizationResult.getCode());
1✔
289
        params.put(REDIRECT_URI, redirectURL);
1✔
290
        params.put("hash", hash);
1✔
291

292
        // Generate the URL and then get the token
293
        return requestToken(QueryUtil.generateUrl(tokenURL, params));
×
294
    }
295

296
    /**
297
     * Refresh token.
298
     * <p>
299
     * Exceptions:
300
     * - IllegalArgumentException : if token is null.
301
     * - InvalidTokenRequestException : if the token request is invalid
302
     * - InvalidOAuthClientException : if the client information is invalid
303
     * - InvalidOAuthGrantException : if the authorization code or refresh token is invalid or expired,
304
     * the redirect_uri does not match, or the hash value does not match the client secret and/or code
305
     * - UnsupportedOAuthGrantTypeException : if the grant type is invalid
306
     * - OAuthTokenException : if any other error occurred during the operation
307
     *
308
     * @param token the token to refresh
309
     * @return the refreshed token
310
     * @throws OAuthTokenException     the o auth token exception
311
     * @throws JSONSerializerException the JSON serializer exception
312
     * @throws HttpClientException     the http client exception
313
     * @throws URISyntaxException      the URI syntax exception
314
     * @throws InvalidRequestException the invalid request exception
315
     */
316
    public Token refreshToken(Token token)
317
            throws OAuthTokenException, JSONSerializerException, HttpClientException, URISyntaxException, InvalidRequestException {
318
        // Prepare the hash
319
        String doHash = clientSecret + "|" + token.getRefreshToken();
1✔
320
        MessageDigest md;
321
        try {
322
            md = MessageDigest.getInstance("SHA-256");
1✔
323
        } catch (NoSuchAlgorithmException e) {
×
324
            throw new RuntimeException("Your JVM does not support SHA-256, which is required for OAuth with Smartsheet.", e);
×
325
        }
1✔
326
        byte[] digest;
327
        digest = md.digest(doHash.getBytes(StandardCharsets.UTF_8));
1✔
328
        String hash = org.apache.commons.codec.binary.Hex.encodeHexString(digest);
1✔
329

330
        // Create a map of the parameters
331
        Map<String, Object> params = new HashMap<>();
1✔
332
        params.put("grant_type", REFRESH_TOKEN);
1✔
333
        params.put(CLIENT_ID, clientId);
1✔
334
        params.put(REFRESH_TOKEN, token.getRefreshToken());
1✔
335
        params.put(REDIRECT_URI, redirectURL);
1✔
336
        params.put("hash", hash);
1✔
337

338
        // Generate the URL and get the token
339
        return requestToken(QueryUtil.generateUrl(tokenURL, params));
×
340
    }
341

342
    /**
343
     * Request a token.
344
     * <p>
345
     * Exceptions:
346
     * - IllegalArgumentException : if url is null or empty
347
     * - InvalidTokenRequestException : if the token request is invalid
348
     * - InvalidOAuthClientException : if the client information is invalid
349
     * - InvalidOAuthGrantException : if the authorization code or refresh token is invalid or
350
     * expired, the redirect_uri does not match, or the hash value does not match the client secret and/or code
351
     * - UnsupportedOAuthGrantTypeException : if the grant type is invalid
352
     * - OAuthTokenException : if any other error occurred during the operation
353
     *
354
     * @param url the URL (with request parameters) from which the token will be requested
355
     * @return the token
356
     * @throws OAuthTokenException     the o auth token exception
357
     * @throws JSONSerializerException the JSON serializer exception
358
     * @throws HttpClientException     the http client exception
359
     * @throws URISyntaxException      the URI syntax exception
360
     */
361
    private Token requestToken(String url) throws OAuthTokenException, JSONSerializerException, HttpClientException,
362
            URISyntaxException {
363

364
        // Create the request and send it to get the response/token.
365
        HttpRequest request = new HttpRequest();
1✔
366
        request.setUri(new URI(url));
1✔
367
        request.setMethod(HttpMethod.POST);
1✔
368
        request.setHeaders(new HashMap<>());
1✔
369
        request.getHeaders().put("Content-Type", "application/x-www-form-urlencoded");
1✔
370
        HttpResponse response = httpClient.request(request);
1✔
371

372
        // Create a map of the response
373
        InputStream inputStream = response.getEntity().getContent();
1✔
374
        Map<String, Object> map = jsonSerializer.deserializeMap(inputStream);
1✔
375
        httpClient.releaseConnection();
1✔
376

377
        // Check for a error response and throw it.
378
        if (response.getStatusCode() != 200 && map.get(ERROR) != null) {
1✔
379
            String errorType = map.get(ERROR).toString();
1✔
380
            String errorDescription = map.get("message") == null ? "" : (String) map.get("message");
1✔
381
            if ("invalid_request".equals(errorType)) {
1✔
382
                throw new InvalidTokenRequestException(errorDescription);
×
383
            } else if ("invalid_client".equals(errorType)) {
1✔
384
                throw new InvalidOAuthClientException(errorDescription);
1✔
385
            } else if ("invalid_grant".equals(errorType)) {
×
386
                throw new InvalidOAuthGrantException(errorDescription);
×
387
            } else if ("unsupported_grant_type".equals(errorType)) {
×
388
                throw new UnsupportedOAuthGrantTypeException(errorDescription);
×
389
            } else {
390
                throw new OAuthTokenException(errorDescription);
×
391
            }
392
        } else if (response.getStatusCode() != 200) {
1✔
393
            // Another error by not getting a 200 result
394
            throw new OAuthTokenException("Token request failed with http error code: " + response.getStatusCode());
1✔
395
        }
396

397
        // Create a token based on the response
398
        Token token = new Token();
×
399
        Object tempObj = map.get("access_token");
×
400
        token.setAccessToken(tempObj == null ? "" : (String) tempObj);
×
401
        tempObj = map.get("token_type");
×
402
        token.setTokenType(tempObj == null ? "" : (String) tempObj);
×
403
        tempObj = map.get(REFRESH_TOKEN);
×
404
        token.setRefreshToken(tempObj == null ? "" : (String) tempObj);
×
405

406
        Long expiresIn;
407
        try {
408
            expiresIn = Long.parseLong(String.valueOf(map.get("expires_in")));
×
409
        } catch (NumberFormatException nfe) {
×
410
            expiresIn = 0L;
×
411
        }
×
412
        token.setExpiresInSeconds(expiresIn);
×
413

414
        return token;
×
415
    }
416

417
    /**
418
     * Revoke access token.
419
     * <p>
420
     * Exceptions:
421
     * - IllegalArgumentException : if url is null or empty
422
     * - InvalidTokenRequestException : if the token request is invalid
423
     * - InvalidOAuthClientException : if the client information is invalid
424
     * - InvalidOAuthGrantException : if the authorization code or refresh token is invalid or
425
     * expired, the redirect_uri does not match, or the hash value does not match the client secret and/or code
426
     * - UnsupportedOAuthGrantTypeException : if the grant type is invalid
427
     * - OAuthTokenException : if any other error occurred during the operation
428
     *
429
     * @param token the access token to revoke access from
430
     * @throws OAuthTokenException     the o auth token exception
431
     * @throws JSONSerializerException the JSON serializer exception
432
     * @throws HttpClientException     the http client exception
433
     * @throws URISyntaxException      the URI syntax exception
434
     * @throws InvalidRequestException the invalid request exception
435
     */
436
    public void revokeAccessToken(Token token) throws OAuthTokenException, JSONSerializerException, HttpClientException,
437
            URISyntaxException, InvalidRequestException {
438
        HttpRequest request = new HttpRequest();
1✔
439
        request.setUri(new URI(tokenURL));
1✔
440
        request.setMethod(HttpMethod.DELETE);
1✔
441

442
        request.setHeaders(new HashMap<>());
1✔
443
        request.getHeaders().put("Authorization", "Bearer " + token.getAccessToken());
1✔
444
        HttpResponse response = httpClient.request(request);
1✔
445

446
        if (response.getStatusCode() != 200) {
1✔
447
            throw new OAuthTokenException("Token request failed with http error code: " + response.getStatusCode());
1✔
448
        }
449

450
        httpClient.releaseConnection();
×
451
    }
×
452

453
    /**
454
     * Gets the http client.
455
     *
456
     * @return the http client
457
     */
458
    public HttpClient getHttpClient() {
459
        return httpClient;
1✔
460
    }
461

462
    /**
463
     * Sets the http client.
464
     *
465
     * @param httpClient the new http client
466
     */
467
    public void setHttpClient(HttpClient httpClient) {
468
        this.httpClient = httpClient;
×
469
    }
×
470

471
    /**
472
     * Gets the json serializer.
473
     *
474
     * @return the json serializer
475
     */
476
    public JsonSerializer getJsonSerializer() {
477
        return jsonSerializer;
1✔
478
    }
479

480
    /**
481
     * Sets the json serializer.
482
     *
483
     * @param jsonSerializer the new json serializer
484
     */
485
    public void setJsonSerializer(JsonSerializer jsonSerializer) {
486
        this.jsonSerializer = jsonSerializer;
×
487
    }
×
488

489
    /**
490
     * Gets the client id.
491
     *
492
     * @return the client id
493
     */
494
    public String getClientId() {
495
        return clientId;
1✔
496
    }
497

498
    /**
499
     * Sets the client id.
500
     *
501
     * @param clientId the new client id
502
     */
503
    public void setClientId(String clientId) {
504
        this.clientId = clientId;
×
505
    }
×
506

507
    /**
508
     * Gets the client secret.
509
     *
510
     * @return the client secret
511
     */
512
    public String getClientSecret() {
513
        return clientSecret;
1✔
514
    }
515

516
    /**
517
     * Sets the client secret.
518
     *
519
     * @param clientSecret the new client secret
520
     */
521
    public void setClientSecret(String clientSecret) {
522
        this.clientSecret = clientSecret;
×
523
    }
×
524

525
    /**
526
     * Gets the redirect url.
527
     *
528
     * @return the redirect url
529
     */
530
    public String getRedirectURL() {
531
        return redirectURL;
1✔
532
    }
533

534
    /**
535
     * Sets the redirect url.
536
     *
537
     * @param redirectURL the new redirect url
538
     */
539
    public void setRedirectURL(String redirectURL) {
540
        this.redirectURL = redirectURL;
×
541
    }
×
542

543
    /**
544
     * Gets the authorization url.
545
     *
546
     * @return the authorization url
547
     */
548
    public String getAuthorizationURL() {
549
        return authorizationURL;
1✔
550
    }
551

552
    /**
553
     * Sets the authorization url.
554
     *
555
     * @param authorizationURL the new authorization url
556
     */
557
    public void setAuthorizationURL(String authorizationURL) {
558
        this.authorizationURL = authorizationURL;
×
559
    }
×
560

561
    /**
562
     * Gets the token url.
563
     *
564
     * @return the token url
565
     */
566
    public String getTokenURL() {
567
        return tokenURL;
1✔
568
    }
569

570
    /**
571
     * Sets the token url.
572
     *
573
     * @param tokenURL the new token url
574
     */
575
    public void setTokenURL(String tokenURL) {
576
        this.tokenURL = tokenURL;
1✔
577
    }
1✔
578
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc