com.smartsheet.api.internal.oauth.OAuthFlowImpl Maven / Gradle / Ivy
Show all versions of smartsheet-sdk-java Show documentation
/*
* Copyright (C) 2023 Smartsheet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.smartsheet.api.internal.oauth;
import com.smartsheet.api.InvalidRequestException;
import com.smartsheet.api.internal.http.HttpClient;
import com.smartsheet.api.internal.http.HttpClientException;
import com.smartsheet.api.internal.http.HttpMethod;
import com.smartsheet.api.internal.http.HttpRequest;
import com.smartsheet.api.internal.http.HttpResponse;
import com.smartsheet.api.internal.json.JSONSerializerException;
import com.smartsheet.api.internal.json.JsonSerializer;
import com.smartsheet.api.internal.util.QueryUtil;
import com.smartsheet.api.internal.util.Util;
import com.smartsheet.api.oauth.AccessDeniedException;
import com.smartsheet.api.oauth.AccessScope;
import com.smartsheet.api.oauth.AuthorizationResult;
import com.smartsheet.api.oauth.InvalidOAuthClientException;
import com.smartsheet.api.oauth.InvalidOAuthGrantException;
import com.smartsheet.api.oauth.InvalidScopeException;
import com.smartsheet.api.oauth.InvalidTokenRequestException;
import com.smartsheet.api.oauth.OAuthAuthorizationCodeException;
import com.smartsheet.api.oauth.OAuthFlow;
import com.smartsheet.api.oauth.OAuthTokenException;
import com.smartsheet.api.oauth.Token;
import com.smartsheet.api.oauth.UnsupportedOAuthGrantTypeException;
import com.smartsheet.api.oauth.UnsupportedResponseTypeException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/**
* Default implementation of OAuthFlow.
*
* Thread Safety: Implementation of this interface must be thread safe.
*/
public class OAuthFlowImpl implements OAuthFlow {
/**
* Represents the HttpClient.
*
* It will be initialized in constructor and will not change afterwards.
*/
private HttpClient httpClient;
/**
* Represents the JsonSerializer.
*
* It will be initialized in constructor and will not change afterwards.
*/
private JsonSerializer jsonSerializer;
/**
* Represents the Client ID.
*
* It will be initialized in constructor and will not change afterwards.
*/
private String clientId;
/**
* Represents the Client Secret.
*
* It will be initialized in constructor and will not change afterwards.
*/
private String clientSecret;
/**
* Represents the redirect URL.
*
* It will be initialized in constructor and will not change afterwards.
*/
private String redirectURL;
/**
* Represents the authorization URL.
*
* It will be initialized in constructor and will not change afterwards.
*/
private String authorizationURL;
/**
* Represents the token URL.
*
* It will be initialized in constructor and will not change afterwards.
*/
private String tokenURL;
private static final String CODE = "code";
private static final String CLIENT_ID = "client_id";
private static final String REDIRECT_URI = "redirect_uri";
private static final String ERROR = "error";
private static final String REFRESH_TOKEN = "refresh_token";
/**
* Constructor.
*
* @param clientId the client id
* @param clientSecret the client secret
* @param redirectURL the redirect url
* @param authorizationURL the authorization url
* @param tokenURL the token url
* @param httpClient the http client
* @param jsonSerializer the json serializer
* @throws IllegalArgumentException If any argument is null, or empty string.
*/
public OAuthFlowImpl(String clientId, String clientSecret, String redirectURL, String authorizationURL,
String tokenURL, HttpClient httpClient, JsonSerializer jsonSerializer) {
Util.throwIfNull(clientId, clientSecret, redirectURL, authorizationURL, tokenURL, httpClient, jsonSerializer);
Util.throwIfEmpty(clientId, clientSecret, redirectURL, authorizationURL, tokenURL);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.redirectURL = redirectURL;
this.authorizationURL = authorizationURL;
this.tokenURL = tokenURL;
this.httpClient = httpClient;
this.jsonSerializer = jsonSerializer;
}
/**
* Generate a new authorization URL.
*
* Exceptions: - IllegalArgumentException : if scopes is null/empty
*
* @param scopes the scopes
* @param state an arbitrary string that will be returned to your app; intended to be used by you to ensure that
* this redirect is indeed from an OAuth flow that you initiated
* @return the authorization URL
*/
public String newAuthorizationURL(EnumSet scopes, String state) {
Util.throwIfNull(scopes);
if (state == null) {
state = "";
}
// Build a map of parameters for the URL
Map params = new HashMap<>();
params.put("response_type", CODE);
params.put(CLIENT_ID, clientId);
params.put(REDIRECT_URI, redirectURL);
params.put("state", state);
StringBuilder scopeBuffer = new StringBuilder();
for (AccessScope scope : scopes) {
scopeBuffer.append(scope.name() + ",");
}
params.put("scope", scopeBuffer.substring(0, scopeBuffer.length() - 1));
// Generate the URL with the parameters
return QueryUtil.generateUrl(authorizationURL, params);
}
/**
* Extract AuthorizationResult from the authorization response URL (i.e. the redirectURL with the response
* parameters from Smartsheet OAuth server).
*
* Exceptions:
*
* - IllegalArgumentException : if authorizationResponseURL is null/empty, or a malformed URL
*
* - AccessDeniedException : if the user has denied the authorization request
*
* - UnsupportedResponseTypeException : if the response type isn't supported
* (note that this won't really happen in current implementation)
*
* - InvalidScopeException : if some of the specified scopes are invalid
*
* - OAuthAuthorizationCodeException : if any other error occurred during the operation
*
* @param authorizationResponseURL the authorization response URL
* @return the authorization result
* @throws URISyntaxException the URI syntax exception
* @throws OAuthAuthorizationCodeException the o auth authorization code exception
*/
public AuthorizationResult extractAuthorizationResult(String authorizationResponseURL)
throws URISyntaxException, OAuthAuthorizationCodeException {
Util.throwIfNull(authorizationResponseURL);
Util.throwIfEmpty(authorizationResponseURL);
// Get all of the parms from the URL
URI uri = new URI(authorizationResponseURL);
String query = uri.getQuery();
if (query == null) {
throw new OAuthAuthorizationCodeException("There must be a query string in the response URL");
}
Map map = new HashMap<>();
for (String param : query.split("&")) {
int index = param.indexOf('=');
map.put(param.substring(0, index), param.substring(index + 1));
}
// Check for an error response in the URL and throw it.
String error = map.get(ERROR);
if (error != null && !error.isEmpty()) {
if ("access_denied".equals(error)) {
throw new AccessDeniedException("Access denied.");
} else if ("unsupported_response_type".equals(error)) {
throw new UnsupportedResponseTypeException("response_type must be set to \"code\".");
} else if ("invalid_scope".equals(error)) {
throw new InvalidScopeException("One or more of the requested access scopes are invalid. " +
"Please check the list of access scopes");
} else {
throw new OAuthAuthorizationCodeException("An undefined error was returned of type: " + error);
}
}
AuthorizationResult authorizationResult = new AuthorizationResult();
authorizationResult.setCode(map.get(CODE));
authorizationResult.setState(map.get("state"));
Long expiresIn;
try {
expiresIn = Long.parseLong(map.get("expires_in"));
} catch (NumberFormatException ex) {
expiresIn = 0L;
}
authorizationResult.setExpiresInSeconds(expiresIn);
return authorizationResult;
}
/**
* Obtain a new token using AuthorizationResult.
*
* Exceptions:
* - IllegalArgumentException : if authorizationResult is null
* - InvalidTokenRequestException : if the token request is invalid (note that this won't really happen in current implementation)
* - InvalidOAuthClientException : if the client information is invalid
* - InvalidOAuthGrantException : if the authorization code or refresh token is invalid or expired, the
* redirect_uri does not match, or the hash value does not match the client secret and/or code
* - UnsupportedOAuthGrantTypeException : if the grant type is invalid (note that this won't really happen in
* current implementation)
* - OAuthTokenException : if any other error occurred during the operation
*
* @param authorizationResult the authorization result
* @return the token
* @throws OAuthTokenException the o auth token exception
* @throws JSONSerializerException the JSON serializer exception
* @throws HttpClientException the http client exception
* @throws URISyntaxException the URI syntax exception
* @throws InvalidRequestException the invalid request exception
*/
public Token obtainNewToken(AuthorizationResult authorizationResult)
throws OAuthTokenException, JSONSerializerException, HttpClientException, URISyntaxException, InvalidRequestException {
if (authorizationResult == null) {
throw new IllegalArgumentException();
}
// Prepare the hash
String doHash = clientSecret + "|" + authorizationResult.getCode();
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Your JVM does not support SHA-256, which is required for OAuth with Smartsheet.", e);
}
byte[] digest;
digest = md.digest(doHash.getBytes(StandardCharsets.UTF_8));
//String hash = javax.xml.bind.DatatypeConverter.printHexBinary(digest);
String hash = org.apache.commons.codec.binary.Hex.encodeHexString(digest);
// create a Map of the parameters
Map params = new HashMap<>();
params.put("grant_type", "authorization_code");
params.put(CLIENT_ID, clientId);
params.put(CODE, authorizationResult.getCode());
params.put(REDIRECT_URI, redirectURL);
params.put("hash", hash);
// Generate the URL and then get the token
return requestToken(QueryUtil.generateUrl(tokenURL, params));
}
/**
* Refresh token.
*
* Exceptions:
* - IllegalArgumentException : if token is null.
* - InvalidTokenRequestException : if the token request is invalid
* - InvalidOAuthClientException : if the client information is invalid
* - InvalidOAuthGrantException : if the authorization code or refresh token is invalid or expired,
* the redirect_uri does not match, or the hash value does not match the client secret and/or code
* - UnsupportedOAuthGrantTypeException : if the grant type is invalid
* - OAuthTokenException : if any other error occurred during the operation
*
* @param token the token to refresh
* @return the refreshed token
* @throws OAuthTokenException the o auth token exception
* @throws JSONSerializerException the JSON serializer exception
* @throws HttpClientException the http client exception
* @throws URISyntaxException the URI syntax exception
* @throws InvalidRequestException the invalid request exception
*/
public Token refreshToken(Token token)
throws OAuthTokenException, JSONSerializerException, HttpClientException, URISyntaxException, InvalidRequestException {
// Prepare the hash
String doHash = clientSecret + "|" + token.getRefreshToken();
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Your JVM does not support SHA-256, which is required for OAuth with Smartsheet.", e);
}
byte[] digest;
digest = md.digest(doHash.getBytes(StandardCharsets.UTF_8));
//String hash = javax.xml.bind.DatatypeConverter.printHexBinary(digest);
String hash = org.apache.commons.codec.binary.Hex.encodeHexString(digest);
// Create a map of the parameters
Map params = new HashMap<>();
params.put("grant_type", REFRESH_TOKEN);
params.put(CLIENT_ID, clientId);
params.put(REFRESH_TOKEN, token.getRefreshToken());
params.put(REDIRECT_URI, redirectURL);
params.put("hash", hash);
// Generate the URL and get the token
return requestToken(QueryUtil.generateUrl(tokenURL, params));
}
/**
* Request a token.
*
* Exceptions:
* - IllegalArgumentException : if url is null or empty
* - InvalidTokenRequestException : if the token request is invalid
* - InvalidOAuthClientException : if the client information is invalid
* - InvalidOAuthGrantException : if the authorization code or refresh token is invalid or
* expired, the redirect_uri does not match, or the hash value does not match the client secret and/or code
* - UnsupportedOAuthGrantTypeException : if the grant type is invalid
* - OAuthTokenException : if any other error occurred during the operation
*
* @param url the URL (with request parameters) from which the token will be requested
* @return the token
* @throws OAuthTokenException the o auth token exception
* @throws JSONSerializerException the JSON serializer exception
* @throws HttpClientException the http client exception
* @throws URISyntaxException the URI syntax exception
* @throws InvalidRequestException the invalid request exception
*/
private Token requestToken(String url) throws OAuthTokenException, JSONSerializerException, HttpClientException,
URISyntaxException, InvalidRequestException {
// Create the request and send it to get the response/token.
HttpRequest request = new HttpRequest();
request.setUri(new URI(url));
request.setMethod(HttpMethod.POST);
request.setHeaders(new HashMap<>());
request.getHeaders().put("Content-Type", "application/x-www-form-urlencoded");
HttpResponse response = httpClient.request(request);
// Create a map of the response
InputStream inputStream = response.getEntity().getContent();
Map map = jsonSerializer.deserializeMap(inputStream);
httpClient.releaseConnection();
// Check for a error response and throw it.
if (response.getStatusCode() != 200 && map.get(ERROR) != null) {
String errorType = map.get(ERROR).toString();
String errorDescription = map.get("message") == null ? "" : (String) map.get("message");
if ("invalid_request".equals(errorType)) {
throw new InvalidTokenRequestException(errorDescription);
} else if ("invalid_client".equals(errorType)) {
throw new InvalidOAuthClientException(errorDescription);
} else if ("invalid_grant".equals(errorType)) {
throw new InvalidOAuthGrantException(errorDescription);
} else if ("unsupported_grant_type".equals(errorType)) {
throw new UnsupportedOAuthGrantTypeException(errorDescription);
} else {
throw new OAuthTokenException(errorDescription);
}
} else if (response.getStatusCode() != 200) {
// Another error by not getting a 200 result
throw new OAuthTokenException("Token request failed with http error code: " + response.getStatusCode());
}
// Create a token based on the response
Token token = new Token();
Object tempObj = map.get("access_token");
token.setAccessToken(tempObj == null ? "" : (String) tempObj);
tempObj = map.get("token_type");
token.setTokenType(tempObj == null ? "" : (String) tempObj);
tempObj = map.get(REFRESH_TOKEN);
token.setRefreshToken(tempObj == null ? "" : (String) tempObj);
Long expiresIn;
try {
expiresIn = Long.parseLong(String.valueOf(map.get("expires_in")));
} catch (NumberFormatException nfe) {
expiresIn = 0L;
}
token.setExpiresInSeconds(expiresIn);
return token;
}
/**
* Revoke access token.
*
* Exceptions:
* - IllegalArgumentException : if url is null or empty
* - InvalidTokenRequestException : if the token request is invalid
* - InvalidOAuthClientException : if the client information is invalid
* - InvalidOAuthGrantException : if the authorization code or refresh token is invalid or
* expired, the redirect_uri does not match, or the hash value does not match the client secret and/or code
* - UnsupportedOAuthGrantTypeException : if the grant type is invalid
* - OAuthTokenException : if any other error occurred during the operation
*
* @param token the access token to revoke access from
* @throws OAuthTokenException the o auth token exception
* @throws JSONSerializerException the JSON serializer exception
* @throws HttpClientException the http client exception
* @throws URISyntaxException the URI syntax exception
* @throws InvalidRequestException the invalid request exception
*/
public void revokeAccessToken(Token token) throws OAuthTokenException, JSONSerializerException, HttpClientException,
URISyntaxException, InvalidRequestException {
HttpRequest request = new HttpRequest();
request.setUri(new URI(tokenURL));
request.setMethod(HttpMethod.DELETE);
request.setHeaders(new HashMap<>());
request.getHeaders().put("Authorization", "Bearer " + token.getAccessToken());
HttpResponse response = httpClient.request(request);
if (response.getStatusCode() != 200) {
throw new OAuthTokenException("Token request failed with http error code: " + response.getStatusCode());
}
httpClient.releaseConnection();
}
/**
* Gets the http client.
*
* @return the http client
*/
public HttpClient getHttpClient() {
return httpClient;
}
/**
* Sets the http client.
*
* @param httpClient the new http client
*/
public void setHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* Gets the json serializer.
*
* @return the json serializer
*/
public JsonSerializer getJsonSerializer() {
return jsonSerializer;
}
/**
* Sets the json serializer.
*
* @param jsonSerializer the new json serializer
*/
public void setJsonSerializer(JsonSerializer jsonSerializer) {
this.jsonSerializer = jsonSerializer;
}
/**
* Gets the client id.
*
* @return the client id
*/
public String getClientId() {
return clientId;
}
/**
* Sets the client id.
*
* @param clientId the new client id
*/
public void setClientId(String clientId) {
this.clientId = clientId;
}
/**
* Gets the client secret.
*
* @return the client secret
*/
public String getClientSecret() {
return clientSecret;
}
/**
* Sets the client secret.
*
* @param clientSecret the new client secret
*/
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
/**
* Gets the redirect url.
*
* @return the redirect url
*/
public String getRedirectURL() {
return redirectURL;
}
/**
* Sets the redirect url.
*
* @param redirectURL the new redirect url
*/
public void setRedirectURL(String redirectURL) {
this.redirectURL = redirectURL;
}
/**
* Gets the authorization url.
*
* @return the authorization url
*/
public String getAuthorizationURL() {
return authorizationURL;
}
/**
* Sets the authorization url.
*
* @param authorizationURL the new authorization url
*/
public void setAuthorizationURL(String authorizationURL) {
this.authorizationURL = authorizationURL;
}
/**
* Gets the token url.
*
* @return the token url
*/
public String getTokenURL() {
return tokenURL;
}
/**
* Sets the token url.
*
* @param tokenURL the new token url
*/
public void setTokenURL(String tokenURL) {
this.tokenURL = tokenURL;
}
}