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

smartsheet / smartsheet-java-sdk / #41

24 Aug 2023 04:59PM UTC coverage: 50.458% (+0.01%) from 50.444%
#41

push

github-actions

web-flow
Fix Checkstyle Violations in "Impl" Classes (#53)

241 of 241 new or added lines in 32 files covered. (100.0%)

3417 of 6772 relevant lines covered (50.46%)

0.5 hits per line

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

0.0
/src/main/java/com/smartsheet/api/internal/oauth/OAuthFlowImpl.java
1
package com.smartsheet.api.internal.oauth;
2

3
/*
4
 * #[license]
5
 * Smartsheet SDK for Java
6
 * %%
7
 * Copyright (C) 2023 Smartsheet
8
 * %%
9
 * Licensed under the Apache License, Version 2.0 (the "License");
10
 * you may not use this file except in compliance with the License.
11
 * You may obtain a copy of the License at
12
 *
13
 *      http://www.apache.org/licenses/LICENSE-2.0
14
 *
15
 * Unless required by applicable law or agreed to in writing, software
16
 * distributed under the License is distributed on an "AS IS" BASIS,
17
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
 * See the License for the specific language governing permissions and
19
 * limitations under the License.
20
 * %[license]
21
 */
22

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

47
import java.io.InputStream;
48
import java.net.URI;
49
import java.net.URISyntaxException;
50
import java.nio.charset.StandardCharsets;
51
import java.security.MessageDigest;
52
import java.security.NoSuchAlgorithmException;
53
import java.util.EnumSet;
54
import java.util.HashMap;
55
import java.util.Map;
56

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

70
    /**
71
     * Represents the JsonSerializer.
72
     *
73
     * It will be initialized in constructor and will not change afterwards.
74
     */
75
    private JsonSerializer jsonSerializer;
76

77
    /**
78
     * Represents the Client ID.
79
     *
80
     * It will be initialized in constructor and will not change afterwards.
81
     */
82
    private String clientId;
83

84
    /**
85
     * Represents the Client Secret.
86
     *
87
     * It will be initialized in constructor and will not change afterwards.
88
     */
89
    private String clientSecret;
90

91
    /**
92
     * Represents the redirect URL.
93
     *
94
     * It will be initialized in constructor and will not change afterwards.
95
     */
96
    private String redirectURL;
97

98
    /**
99
     * Represents the authorization URL.
100
     *
101
     * It will be initialized in constructor and will not change afterwards.
102
     */
103
    private String authorizationURL;
104

105
    /**
106
     * Represents the token URL.
107
     *
108
     * It will be initialized in constructor and will not change afterwards.
109
     */
110
    private String tokenURL;
111

112
    /**
113
     * Constructor.
114
     *
115
     * Exceptions: -
116
     *
117
     * @param clientId the client id
118
     * @param clientSecret the client secret
119
     * @param redirectURL the redirect url
120
     * @param authorizationURL the authorization url
121
     * @param tokenURL the token url
122
     * @param httpClient the http client
123
     * @param jsonSerializer the json serializer
124
     * @throws IllegalArgumentException If any argument is null, or empty string.
125
     */
126
    public OAuthFlowImpl(String clientId, String clientSecret, String redirectURL, String authorizationURL,
127
                         String tokenURL, HttpClient httpClient, JsonSerializer jsonSerializer) {
×
128
        Util.throwIfNull(clientId, clientSecret, redirectURL, authorizationURL, tokenURL, httpClient, jsonSerializer);
×
129
        Util.throwIfEmpty(clientId, clientSecret, redirectURL, authorizationURL, tokenURL);
×
130

131
        this.clientId = clientId;
×
132
        this.clientSecret = clientSecret;
×
133
        this.redirectURL = redirectURL;
×
134
        this.authorizationURL = authorizationURL;
×
135
        this.tokenURL = tokenURL;
×
136
        this.httpClient = httpClient;
×
137
        this.jsonSerializer = jsonSerializer;
×
138
    }
×
139

140
    /**
141
     * Generate a new authorization URL.
142
     * <p>
143
     * Exceptions: - IllegalArgumentException : if scopes is null/empty
144
     *
145
     * @param scopes the scopes
146
     * @param state an arbitrary string that will be returned to your app; intended to be used by you to ensure that
147
     *              this redirect is indeed from an OAuth flow that you initiated
148
     * @return the authorization URL
149
     */
150
    public String newAuthorizationURL(EnumSet<AccessScope> scopes, String state) {
151
        Util.throwIfNull(scopes);
×
152
        if (state == null) {
×
153
            state = "";
×
154
        }
155

156
        // Build a map of parameters for the URL
157
        Map<String, Object> params = new HashMap<>();
×
158
        params.put("response_type", "code");
×
159
        params.put("client_id", clientId);
×
160
        params.put("redirect_uri", redirectURL);
×
161
        params.put("state", state);
×
162

163
        StringBuilder scopeBuffer = new StringBuilder();
×
164
        for (AccessScope scope : scopes) {
×
165
            scopeBuffer.append(scope.name() + ",");
×
166
        }
×
167
        params.put("scope", scopeBuffer.substring(0, scopeBuffer.length() - 1));
×
168

169
        // Generate the URL with the parameters
170
        return QueryUtil.generateUrl(authorizationURL, params);
×
171
    }
172

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

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

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

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

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

239
        return authorizationResult;
×
240
    }
241

242
    /**
243
     * Obtain a new token using AuthorizationResult.
244
     *
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) {
×
266
            throw new IllegalArgumentException();
×
267
        }
268

269
        // Prepare the hash
270

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

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

282
        //String hash = javax.xml.bind.DatatypeConverter.printHexBinary(digest);
283
        String hash = org.apache.commons.codec.binary.Hex.encodeHexString(digest);
×
284

285
        // create a Map of the parameters
286
        Map<String, Object> params = new HashMap<>();
×
287
        params.put("grant_type", "authorization_code");
×
288
        params.put("client_id", clientId);
×
289
        params.put("code", authorizationResult.getCode());
×
290
        params.put("redirect_uri", redirectURL);
×
291
        params.put("hash", hash);
×
292

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

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

332
        // Create a map of the parameters
333
        Map<String, Object> params = new HashMap<>();
×
334
        params.put("grant_type", "refresh_token");
×
335
        params.put("client_id", clientId);
×
336
        params.put("refresh_token", token.getRefreshToken());
×
337
        params.put("redirect_uri", redirectURL);
×
338
        params.put("hash", hash);
×
339

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

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

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

375
        // Create a map of the response
376
        InputStream inputStream = response.getEntity().getContent();
×
377
        Map<String, Object> map = jsonSerializer.deserializeMap(inputStream);
×
378
        httpClient.releaseConnection();
×
379

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

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

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

417
        return token;
×
418
    }
419

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

445
        request.setHeaders(new HashMap<>());
×
446
        request.getHeaders().put("Authorization", "Bearer " + token.getAccessToken());
×
447
        HttpResponse response = httpClient.request(request);
×
448

449
        if (response.getStatusCode() != 200) {
×
450
            throw new OAuthTokenException("Token request failed with http error code: " + response.getStatusCode());
×
451
        }
452

453
        httpClient.releaseConnection();
×
454
    }
×
455

456
    /**
457
     * Gets the http client.
458
     *
459
     * @return the http client
460
     */
461
    public HttpClient getHttpClient() {
462
        return httpClient;
×
463
    }
464

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

474
    /**
475
     * Gets the json serializer.
476
     *
477
     * @return the json serializer
478
     */
479
    public JsonSerializer getJsonSerializer() {
480
        return jsonSerializer;
×
481
    }
482

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

492
    /**
493
     * Gets the client id.
494
     *
495
     * @return the client id
496
     */
497
    public String getClientId() {
498
        return clientId;
×
499
    }
500

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

510
    /**
511
     * Gets the client secret.
512
     *
513
     * @return the client secret
514
     */
515
    public String getClientSecret() {
516
        return clientSecret;
×
517
    }
518

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

528
    /**
529
     * Gets the redirect url.
530
     *
531
     * @return the redirect url
532
     */
533
    public String getRedirectURL() {
534
        return redirectURL;
×
535
    }
536

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

546
    /**
547
     * Gets the authorization url.
548
     *
549
     * @return the authorization url
550
     */
551
    public String getAuthorizationURL() {
552
        return authorizationURL;
×
553
    }
554

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

564
    /**
565
     * Gets the token url.
566
     *
567
     * @return the token url
568
     */
569
    public String getTokenURL() {
570
        return tokenURL;
×
571
    }
572

573
    /**
574
     * Sets the token url.
575
     *
576
     * @param tokenURL the new token url
577
     */
578
    public void setTokenURL(String tokenURL) {
579
        this.tokenURL = tokenURL;
×
580
    }
×
581
}
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

© 2026 Coveralls, Inc