com.amazon.redshift.plugin.BrowserIdcAuthPlugin Maven / Gradle / Ivy
Show all versions of redshift-jdbc42 Show documentation
/**
* Copyright 2010-2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* This file is licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License. A copy of
* the License is located at
*
* http://aws.amazon.com/apache2.0/
*
* This file 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.amazon.redshift.plugin;
import com.amazon.redshift.logger.LogLevel;
import com.amazon.redshift.logger.RedshiftLogger;
import com.amazon.redshift.plugin.httpserver.RequestHandler;
import com.amazon.redshift.plugin.httpserver.Server;
import com.amazon.redshift.plugin.utils.RandomStateUtil;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;
import java.awt.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.List;
import java.security.SecureRandom;
import java.util.Base64;
import com.amazon.redshift.NativeTokenHolder;
import com.amazon.redshift.RedshiftProperty;
import com.amazonaws.services.ssooidc.AWSSSOOIDC;
import com.amazonaws.services.ssooidc.AWSSSOOIDCClientBuilder;
import com.amazonaws.services.ssooidc.model.*;
import com.amazonaws.util.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import static com.amazon.redshift.plugin.utils.ResponseUtils.findParameter;
public class BrowserIdcAuthPlugin extends CommonCredentialsProvider {
/**
* Key for setting timeout for IDP response.
*/
public static final String KEY_IDC_RESPONSE_TIMEOUT = "idp_response_timeout";
/**
* Key for setting the port number for listening.
*/
public static final String KEY_LISTEN_PORT = "listen_port";
/**
* Key for setting idp tenant.
*/
public static final String KEY_ISSUER_URL = "issuer_url";
/**
* Key for setting IdC region.
*/
public static final String KEY_IDC_REGION = "idc_region";
/**
* Key for setting IdC client display name
*/
private static final String KEY_IDC_CLIENT_DISPLAY_NAME = "idc_client_display_name";
/**
* Key for setting CSRF endpoint protection state.
*/
public static final String OAUTH_CSRF_STATE_PARAMETER_NAME = "state";
/**
* Key for setting redirect URI.
*/
public static final String OAUTH_REDIRECT_PARAMETER_NAME = "redirect_uri";
/**
* Key for setting client ID.
*/
public static final String OAUTH_CLIENT_ID_PARAMETER_NAME = "client_id";
/**
* Key for setting OAUTH response type.
*/
public static final String OAUTH_RESPONSE_TYPE_PARAMETER_NAME = "response_type";
/**
* Key for setting grant type.
*/
public static final String OAUTH_GRANT_TYPE_PARAMETER_NAME = "grant_type";
/**
* Key for setting scope.
*/
public static final String OAUTH_SCOPE_PARAMETER_NAME = "scopes";
/**
* Key for setting code challenge.
*/
public static final String OAUTH_CODE_CHALLENGE_PARAMETER_NAME = "code_challenge";
/**
* Key for setting code challenge.
*/
public static final String OAUTH_CHALLENGE_METHOD_PARAMETER_NAME = "code_challenge_method";
/**
* The default time in seconds for which the client must wait between attempts when polling for a session
*/
public final int CREATE_TOKEN_POLLING_INTERVAL = 1;
/**
* It is used if auth server doesn't provide any value access token expiration
*/
public final int DEFAULT_IDC_TOKEN_EXPIRY_IN_SEC = 900;
/**
* It is used to set the number of bytes of the code verifier
*/
public final int CODE_VERIFIER_BYTE_LENGTH = 60;
/**
* It is used to multiply millisecond values to get seconds
*/
public final long MILLISECOND_MULTIPLIER = 1000L;
/**
* Issuer URL variable.
*/
protected String m_issuer_url;
/**
* IdC region variable.
*/
protected String m_idc_region;
/**
* Redirect URI variable.
*/
protected String m_redirect_uri;
/**
* AWSSSOOIDC client object needed to SSOOIDC methods
*/
protected AWSSSOOIDC m_sdk_client;
/**
* Key for authorization code.
*/
private static final String AUTH_CODE_PARAMETER_NAME = "code";
/**
* String containing HTTPS.
*/
private static final String CURRENT_INTERACTION_PROTOCOL = "https";
/**
* String containing OIDC used for building the authorization server endpoint.
*/
private static final String OIDC_SUBDOMAIN = "oidc";
/**
* String containing amazonaws.com used for building the authorization server endpoint.
*/
private static final String AMAZON_COM_DOMAIN = "amazonaws.com";
/**
* Application scope variable.
*/
private static final String REDSHIFT_IDC_CONNECT_SCOPE = "redshift:connect";
/**
* Application grant types variable.
*/
private static final String AUTH_CODE_GRANT_TYPE = "authorization_code";
/**
* Client type of client application
*/
private static final String M_CLIENT_TYPE = "public";
/**
* Redirect URI of client application
*/
private static final String M_REDIRECT_URI = "http://127.0.0.1";
/**
* Authorize endpoint to get authorization code
*/
private static final String AUTHORIZE_ENDPOINT = "/authorize";
/**
* Method used to hash the code verifier
*/
private static final String CHALLENGE_METHOD = "S256";
/**
* SHA256 hash used to hash the code verifier
*/
private static final String SHA256_METHOD = "SHA-256";
/**
* Default timeout for IDP response.
*/
private int m_idc_response_timeout = 120;
/**
* Default port for local server.
*/
private int m_listen_port = 7890;
/**
* Default IdC client display name.
*/
private String m_idcClientDisplayName = RedshiftProperty.IDC_CLIENT_DISPLAY_NAME.getDefaultValue();
// Used to cache RegisterClientResult, which contains clientId and clientSecret. Cache key will be ::
private static final Map m_register_client_cache = new HashMap();
/**
* Overridden method to obtain the auth token from plugin specific implementation
*
* @return {@link NativeTokenHolder} A wrapper containing auth token and its expiration time information
* @throws IOException indicating the error
*/
@Override
protected NativeTokenHolder getAuthToken() throws IOException {
return getIdcToken();
}
/**
* Returns the retrieved access token from IdC authorization server
*
* @return {@link NativeTokenHolder} This contains the retrieved access token and the expiration time of that token
* @throws IOException if an error occurs during the involved API call
*/
protected NativeTokenHolder getIdcToken() throws IOException {
try {
checkRequiredParameters();
m_sdk_client = AWSSSOOIDCClientBuilder.standard().withRegion(m_idc_region).build();
m_redirect_uri = M_REDIRECT_URI + ":" + m_listen_port;
RegisterClientResult registerClientResult = getRegisterClientResult();
String codeVerifier = generateCodeVerifier();
String codeChallenge = generateCodeChallenge(codeVerifier);
String authCode = fetchAuthorizationCode(codeChallenge, registerClientResult);
CreateTokenResult createTokenResult = fetchTokenResult(registerClientResult, authCode,codeVerifier);
return processCreateTokenResult(createTokenResult);
} catch (InternalPluginException | URISyntaxException ex) {
if (RedshiftLogger.isEnable())
m_log.log(LogLevel.ERROR, ex, "InternalPluginException in getIdcToken");
// Wrap any exception to be compatible with CommonCredentialsProvider API
throw new IOException(ex.getMessage(), ex);
}
}
private void checkRequiredParameters() throws InternalPluginException {
if (StringUtils.isNullOrEmpty(m_issuer_url)) {
m_log.logDebug("IdC authentication failed: issuer_url needs to be provided in connection params");
throw new InternalPluginException("IdC authentication failed: The issuer URL must be included in the connection parameters.");
}
if (StringUtils.isNullOrEmpty(m_idc_region)) {
m_log.logDebug("IdC authentication failed: idc_region needs to be provided in connection params");
throw new InternalPluginException("IdC authentication failed: The IdC region must be included in the connection parameters.");
}
}
/**
* Overridden method to grab the field parameters from JDBC connection string or extended params provided by user.
* This method calls the base class' addParameter method and adds to it new specific parameters.
*
* @param key parameter key passed to JDBC driver
* @param value parameter value associated with the given key
*/
@Override
public void addParameter(String key, String value) {
switch (key) {
case KEY_ISSUER_URL:
m_issuer_url = value;
if (RedshiftLogger.isEnable())
m_log.logDebug("Setting issuer_url: {0}", m_issuer_url);
break;
case KEY_IDC_REGION:
m_idc_region = value;
if (RedshiftLogger.isEnable())
m_log.logDebug("Setting idc_region: {0}", m_idc_region);
break;
case KEY_LISTEN_PORT:
m_listen_port = Integer.parseInt(value);
if (RedshiftLogger.isEnable())
m_log.logDebug("Setting listen_port: {0}", m_listen_port);
break;
case KEY_IDC_CLIENT_DISPLAY_NAME:
if (!StringUtils.isNullOrEmpty(value))
m_idcClientDisplayName = value;
if (RedshiftLogger.isEnable())
m_log.logDebug("Setting idc_client_display_name: {0}", m_idcClientDisplayName);
break;
case KEY_IDC_RESPONSE_TIMEOUT:
if (!StringUtils.isNullOrEmpty(value)) {
int timeout = Integer.parseInt(value);
if (timeout > 10) { // minimum allowed timeout value is 10 secs
m_idc_response_timeout = timeout;
if (RedshiftLogger.isEnable())
m_log.logDebug("Setting idc_response_timeout={0}", m_idc_response_timeout);
} else { // else use default timeout value itself
if (RedshiftLogger.isEnable())
m_log.logDebug("Setting default idc_response_timeout={0}; provided value={1}", m_idc_response_timeout, timeout);
}
}
break;
default:
super.addParameter(key, value);
}
}
/**
* Registers a client with IAM Identity Center. This allows clients to initiate authorization code + PKCE flow.
* The output is persisted for reuse through many authentication requests.
*
* @return {@link RegisterClientResult} Client registration result containing {@code clientId} and {@code clientSecret} required for authorization code + PKCE flow
* @throws IOException if an error occurs during the involved API call
*/
protected RegisterClientResult getRegisterClientResult() throws IOException {
String registerClientCacheKey = m_issuer_url + ":" + m_idc_region + ":" + m_listen_port;
RegisterClientResult cachedRegisterClientResult = m_register_client_cache.get(registerClientCacheKey);
if (isCachedRegisterClientResultValid(cachedRegisterClientResult)) {
if (RedshiftLogger.isEnable()){
m_log.logDebug("Using cached register client result");
m_log.logDebug("Cached register client secret expiry is {0}", cachedRegisterClientResult.getClientSecretExpiresAt());
}
return cachedRegisterClientResult;
}
RegisterClientRequest registerClientRequest = new RegisterClientRequest();
registerClientRequest.withClientName(m_idcClientDisplayName);
registerClientRequest.withClientType(M_CLIENT_TYPE);
registerClientRequest.withScopes(REDSHIFT_IDC_CONNECT_SCOPE);
registerClientRequest.withIssuerUrl(m_issuer_url);
registerClientRequest.withRedirectUris(m_redirect_uri);
registerClientRequest.withGrantTypes(AUTH_CODE_GRANT_TYPE);
RegisterClientResult registerClientResult = null;
try {
registerClientResult = m_sdk_client.registerClient(registerClientRequest);
if (RedshiftLogger.isEnable() && registerClientResult.getSdkHttpMetadata() != null)
m_log.logDebug("registerClient response code: {0}", registerClientResult.getSdkHttpMetadata().getHttpStatusCode());
} catch (InternalServerException ex) {
if (RedshiftLogger.isEnable())
m_log.log(LogLevel.ERROR, ex, "Error: Unexpected server error while registering client;");
throw new IOException("IdC authentication failed : An error occurred during the request.", ex);
} catch (Exception ex) {
if (RedshiftLogger.isEnable())
m_log.log(LogLevel.ERROR, ex, "Error: Unexpected register client error;");
throw new IOException("IdC registerClient failed : There was an error during the request.", ex);
}
m_register_client_cache.put(registerClientCacheKey, registerClientResult);
if (RedshiftLogger.isEnable())
m_log.logDebug("Cached register client secret expiry is {0}", registerClientResult.getClientSecretExpiresAt());
return registerClientResult;
}
/**
* Generates a high entropy random codeVerifier string that will be used in the PKCE flow
*
* @return codeVerifier: randomly generated base64 encoded 60 byte string
*/
protected String generateCodeVerifier() {
byte[] randomBytes = new byte[CODE_VERIFIER_BYTE_LENGTH];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(randomBytes);
String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
return codeVerifier;
}
/**
* Applies a SHA256 hash to the code verifier
*
* @param verifier Randomly generated base64 encoded string
* @return codeChallenge: string generated from applying a SHA256 hash to the code verifier
*/
protected String generateCodeChallenge(String verifier) {
byte[] sha256Hash = sha256(verifier.getBytes(StandardCharsets.US_ASCII));
// Encode the hash into Base64url
String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256Hash);
return codeChallenge;
}
/**
* Retrieves an IdC vended authorization code from the IdC server through the redirectURI
*
* @param codeChallenge String generated from applying a SHA256 hash to the code verifier
* @param registerClientResult Contains the clientId and clientSecret
* @return {@link String} authCode: Authorization code returned from the IdC authorization server used to get the access token
* @throws IOException If an I/O error occurs while communicating with the IdC server or processing the response
* @throws URISyntaxException If the redirect URI or any other URI involved in the authorization process is malformed or violates URI syntax rules
*/
protected String fetchAuthorizationCode(String codeChallenge, RegisterClientResult registerClientResult) throws IOException, URISyntaxException
{
final String state = RandomStateUtil.generateRandomState();
RequestHandler requestHandler =
new RequestHandler(new Function, Object>()
{
@Override
public Object apply(List nameValuePairs)
{
String incomingState =
findParameter(OAUTH_CSRF_STATE_PARAMETER_NAME, nameValuePairs);
if (!state.equals(incomingState))
{
String state_error_message = "Incoming state " + incomingState + " does not match the outgoing state " + state;
m_log.log(LogLevel.DEBUG, state_error_message);
return new InternalPluginException(state_error_message);
}
String code = findParameter(AUTH_CODE_PARAMETER_NAME, nameValuePairs);
if (StringUtils.isNullOrEmpty(code))
{
String code_error_message = "No valid code found";
m_log.log(LogLevel.DEBUG, code_error_message);
return new InternalPluginException(code_error_message);
}
return code;
}
});
Server server = new Server(m_listen_port, requestHandler, Duration.ofSeconds(m_idc_response_timeout), m_log);
try
{
server.listen();
if(RedshiftLogger.isEnable())
m_log.log(LogLevel.DEBUG, String.format("Listening for connection on port %d", m_listen_port));
openBrowser(state, codeChallenge, registerClientResult);
server.waitForResult();
}
catch (URISyntaxException | IOException ex)
{
if (RedshiftLogger.isEnable())
m_log.logError(ex);
server.stop();
throw ex;
}
Object result = requestHandler.getResult();
if (result instanceof InternalPluginException)
{
if (RedshiftLogger.isEnable())
m_log.logDebug("Error occurred while fetching authorization code: {0}", result);
throw (InternalPluginException) result;
}
if (result instanceof String)
{
if(RedshiftLogger.isEnable())
m_log.log(LogLevel.DEBUG, "Got authorization code of length={0}", ((String) result).length());
return (String) result;
}
if (RedshiftLogger.isEnable())
m_log.logDebug("result: {0}", result);
throw new InternalPluginException("Error fetching authentication code from browser. Failed to login during timeout.");
}
/**
* Creates and returns an access token for the authorized client within if successful before the timeout
*
* @param registerClientResult Contains the clientId and clientSecret
* @param authCode Authorization code returned from the IdC authorization server used to get the access token
* @param codeVerifier Randomly generated base64 encoded 60 byte string
* @return {@link CreateTokenResult} Create token result containing IdC token
* @throws IOException If an I/O error occurs while communicating with the IdC server or processing the response
*/
protected CreateTokenResult fetchTokenResult(RegisterClientResult registerClientResult, String authCode, String codeVerifier) throws IOException {
long pollingEndTime = System.currentTimeMillis() + m_idc_response_timeout * MILLISECOND_MULTIPLIER;
int pollingIntervalInSec = CREATE_TOKEN_POLLING_INTERVAL;
// poll for create token with pollingIntervalInSec wait time between each attempt until pollingEndTime
while (System.currentTimeMillis() < pollingEndTime) {
try {
CreateTokenResult createTokenResult = getCreateTokenResult(registerClientResult.getClientId(), registerClientResult.getClientSecret(), authCode, AUTH_CODE_GRANT_TYPE, codeVerifier, m_redirect_uri);
if (RedshiftLogger.isEnable() && registerClientResult.getSdkHttpMetadata() != null)
m_log.logDebug("createToken response code: {0}", createTokenResult.getSdkHttpMetadata().getHttpStatusCode());
if (createTokenResult != null && createTokenResult.getAccessToken() != null) {
return createTokenResult;
} else {
// auth server sent a non exception response without valid token, so throw error
if (RedshiftLogger.isEnable())
m_log.logError("Failed to fetch an IdC access token");
throw new IOException("IdC authentication failed : The credential token couldn't be fetched.");
}
} catch (AuthorizationPendingException ex) {
if (RedshiftLogger.isEnable())
m_log.logDebug("Browser authorization pending from user");
} catch (SlowDownException ex) {
if (RedshiftLogger.isEnable())
m_log.log(LogLevel.ERROR, ex, "Error: Too frequent createToken requests made by client;");
throw new IOException("IdC authentication failed : Requests to the IdC service are too frequent.", ex);
} catch (AccessDeniedException ex) {
if (RedshiftLogger.isEnable())
m_log.log(LogLevel.ERROR, ex, "Error: Access denied, please ensure app assignment is done for the user;");
throw new IOException("IdC authentication failed : You don't have sufficient permission to perform the action. Please ensure app assignment is done for the user.", ex);
} catch (InternalServerException ex) {
if (RedshiftLogger.isEnable())
m_log.log(LogLevel.ERROR, ex, "Error: Server error in creating token;");
throw new IOException("IdC authentication failed : An error occurred during the request.", ex);
} catch (Exception ex) {
if (RedshiftLogger.isEnable())
m_log.log(LogLevel.ERROR, ex, "Error: Unexpected error in create token;");
throw new IOException("IdC createToken failed : There was an error during the request.", ex);
}
try {
Thread.sleep(pollingIntervalInSec * MILLISECOND_MULTIPLIER);
} catch (InterruptedException ex) {
if (RedshiftLogger.isEnable())
m_log.log(LogLevel.ERROR, ex, "Thread interrupted during sleep");
}
}
if (RedshiftLogger.isEnable())
m_log.logError("Error: Request timed out while waiting for user authentication in the browser");
throw new IOException("IdC authentication failed : The request timed out. Authentication wasn't completed.");
}
/**
* Creates and returns an access token for the authorized client.
* The access token issued will be used by the Redshift server to authorize the user.
*
* @param clientId The unique identifier string for each client
* @param clientSecret A secret string generated for the client
* @param authCode Authorization code used to get the access token
* @param grantType Supports grant types for the authorization code request
* @param codeVerifier Used for PKCE flow
* @param redirectUri Used to verify that the redirectUri is the same
* @return {@link CreateTokenResult} Create token result containing IdC token
*/
protected CreateTokenResult getCreateTokenResult(String clientId, String clientSecret, String authCode, String grantType, String codeVerifier, String redirectUri) {
CreateTokenRequest createTokenRequest = new CreateTokenRequest();
createTokenRequest.withClientId(clientId);
createTokenRequest.withClientSecret(clientSecret);
createTokenRequest.withCode(authCode);
createTokenRequest.withGrantType(grantType);
createTokenRequest.withCodeVerifier(codeVerifier);
createTokenRequest.withRedirectUri(redirectUri);
return m_sdk_client.createToken(createTokenRequest);
}
/**
* Takes a created token result as input and returns an object of type NativeTokenHolder that contains the access token and expiration
*
* @param createTokenResult Contains the access token, refresh token, and accces token expiry
* @return {@link NativeTokenHolder} This contains the retrieved access token and the expiration time of that token
*
* @throws IOException If an I/O error occurs while communicating with the IdC server or processing the response
*/
protected NativeTokenHolder processCreateTokenResult(CreateTokenResult createTokenResult) throws IOException {
String idcToken = createTokenResult.getAccessToken();
if(StringUtils.isNullOrEmpty(idcToken)) {
throw new InternalPluginException("Returned access token is null or empty.");
}
int expiresInSecs = DEFAULT_IDC_TOKEN_EXPIRY_IN_SEC;
if (createTokenResult.getExpiresIn() != null && createTokenResult.getExpiresIn() > 0) {
expiresInSecs = createTokenResult.getExpiresIn();
}
Date expiration = new Date(System.currentTimeMillis() + expiresInSecs * MILLISECOND_MULTIPLIER);
if (RedshiftLogger.isEnable())
m_log.logDebug("Access token expires at {0}", expiration);
return NativeTokenHolder.newInstance(idcToken, expiration);
}
/**
* Opens the default browser with the authorization code request to IdC /authorize endpoint
*
* @param state A randomly generated string to protect against cross-site request forgery attacks
* @param codeChallenge String generated from applying a SHA256 hash to the code verifier
* @param registerClientResult Contains the clientId and clientSecret
*
* @throws IOException If an error occurs while opening the default browser or establishing a connection to the IdC /authorize endpoint
* @throws URISyntaxException If the URI used for the authorization code request is malformed or violates URI syntax rules
*/
protected void openBrowser(String state, String codeChallenge, RegisterClientResult registerClientResult) throws URISyntaxException, IOException
{
String idc_host = createIdcHost(m_idc_region);
URIBuilder builder = new URIBuilder().setScheme(CURRENT_INTERACTION_PROTOCOL)
.setHost(idc_host)
.setPath(AUTHORIZE_ENDPOINT)
.addParameter(OAUTH_RESPONSE_TYPE_PARAMETER_NAME, AUTH_CODE_PARAMETER_NAME)
.addParameter(OAUTH_CLIENT_ID_PARAMETER_NAME, registerClientResult.getClientId())
.addParameter(OAUTH_REDIRECT_PARAMETER_NAME, m_redirect_uri)
.addParameter(OAUTH_SCOPE_PARAMETER_NAME, REDSHIFT_IDC_CONNECT_SCOPE)
.addParameter(OAUTH_CSRF_STATE_PARAMETER_NAME, state)
.addParameter(OAUTH_CODE_CHALLENGE_PARAMETER_NAME, codeChallenge)
.addParameter(OAUTH_CHALLENGE_METHOD_PARAMETER_NAME, CHALLENGE_METHOD);
URI authorizeRequestUrl;
authorizeRequestUrl = builder.build();
validateURL(authorizeRequestUrl.toString());
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(authorizeRequestUrl);
} else {
m_log.log(LogLevel.ERROR, "Unable to open the browser. Desktop environment is not supported");
}
if(RedshiftLogger.isEnable())
m_log.log(LogLevel.DEBUG,
String.format("Authorization code request URI: \n%s", authorizeRequestUrl.toString()));
}
private String createIdcHost(String idc_region) {
return OIDC_SUBDOMAIN + "." + m_idc_region + "." + AMAZON_COM_DOMAIN;
}
private byte[] sha256(byte[] input) {
try {
MessageDigest digest = MessageDigest.getInstance(SHA256_METHOD);
return digest.digest(input);
} catch (NoSuchAlgorithmException e) {
if (RedshiftLogger.isEnable())
m_log.log(LogLevel.ERROR, e, "Thread interrupted during sleep");
return null;
}
}
private boolean isCachedRegisterClientResultValid(RegisterClientResult cachedRegisterClientResult) {
if (cachedRegisterClientResult == null || cachedRegisterClientResult.getClientSecretExpiresAt() == null) {
return false;
}
return System.currentTimeMillis() < cachedRegisterClientResult.getClientSecretExpiresAt() * 1000;
}
}