-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #165 from forcedotcom/develop
Merging develop to master for release
- Loading branch information
Showing
34 changed files
with
1,123 additions
and
742 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
src/main/java/com/salesforce/cdp/queryservice/auth/TokenExchangeHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package com.salesforce.cdp.queryservice.auth; | ||
|
||
import com.salesforce.cdp.queryservice.auth.token.CoreToken; | ||
import com.salesforce.cdp.queryservice.auth.token.OffcoreToken; | ||
import com.salesforce.cdp.queryservice.util.Constants; | ||
import com.salesforce.cdp.queryservice.util.HttpHelper; | ||
import com.salesforce.cdp.queryservice.util.TokenException; | ||
import lombok.extern.slf4j.Slf4j; | ||
import okhttp3.FormBody; | ||
import okhttp3.OkHttpClient; | ||
import okhttp3.Request; | ||
import okhttp3.Response; | ||
import org.apache.commons.lang3.StringUtils; | ||
|
||
import java.io.IOException; | ||
import java.util.*; | ||
|
||
import static com.salesforce.cdp.queryservice.util.Messages.FAILED_LOGIN_2; | ||
import static com.salesforce.cdp.queryservice.util.Messages.TOKEN_EXCHANGE_FAILURE; | ||
|
||
@Slf4j | ||
public class TokenExchangeHelper { | ||
|
||
private final Properties properties; | ||
private final OkHttpClient client; | ||
|
||
public TokenExchangeHelper(Properties properties, OkHttpClient client) { | ||
this.properties = properties; | ||
this.client = client; | ||
} | ||
|
||
public OffcoreToken exchangeToken(CoreToken coreToken) throws TokenException { | ||
return exchangeToken(properties.getProperty(Constants.LOGIN_URL), | ||
coreToken.getAccessToken(), properties.getProperty(Constants.DATASPACE)); | ||
} | ||
|
||
private OffcoreToken exchangeToken(String url, String coreToken, String dataspace) throws TokenException { | ||
String token_url = url + Constants.TOKEN_EXCHANGE_URL; | ||
Map<String, String> requestBody = new HashMap<>(); | ||
requestBody.put(Constants.GRANT_TYPE_NAME, Constants.GRANT_TYPE); | ||
requestBody.put(Constants.SUBJECT_TOKEN_TYPE_NAME, Constants.SUBJECT_TOKEN_TYPE); | ||
requestBody.put(Constants.SUBJECT_TOKEN, coreToken); | ||
if(StringUtils.isNotBlank(dataspace)) {requestBody.put(Constants.DATASPACE,dataspace);} | ||
Calendar expireTime = Calendar.getInstance(); | ||
Response response = null; | ||
try { | ||
response = login(requestBody, token_url); | ||
OffcoreToken token = HttpHelper.handleSuccessResponse(response, OffcoreToken.class, false); | ||
if (token.getErrorDescription() != null) { | ||
log.error("Token exchange failed with error {}", token.getErrorDescription()); | ||
TokenUtils.invalidateCoreToken(url, coreToken, client); | ||
String message = token.getErrorDescription(); | ||
throw new TokenException(message); | ||
} | ||
expireTime.add(Calendar.SECOND, token.getExpiresIn()); | ||
token.setExpireTime(expireTime); | ||
return token; | ||
} catch (IOException e) { | ||
log.error("Caught exception while exchanging the offcore token", e); | ||
TokenUtils.invalidateCoreToken(url, coreToken, client); | ||
throw new TokenException(TOKEN_EXCHANGE_FAILURE, e); | ||
} | ||
} | ||
|
||
private Response login(Map<String, String> requestBody, String url) throws TokenException { | ||
FormBody.Builder formBody = new FormBody.Builder(); | ||
requestBody.entrySet().stream().filter(e -> e.getValue() != null).forEach(e -> formBody.addEncoded(e.getKey(), e.getValue())); | ||
Map<String, String> headers = Collections.singletonMap(Constants.CONTENT_TYPE, Constants.URL_ENCODED_CONTENT); | ||
log.info(requestBody.toString()); | ||
try { | ||
Request request = HttpHelper.buildRequest(Constants.POST, url, formBody.build(), headers); | ||
Response response = client.newCall(request).execute(); | ||
if (!response.isSuccessful()) { | ||
log.error("login failed with status code {}", response.code()); | ||
HttpHelper.handleErrorResponse(response, Constants.ERROR_DESCRIPTION); | ||
} | ||
return response; | ||
} catch (IOException e) { | ||
log.error("login failed", e); | ||
throw new TokenException(FAILED_LOGIN_2, e); | ||
} | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
src/main/java/com/salesforce/cdp/queryservice/auth/TokenManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.salesforce.cdp.queryservice.auth; | ||
|
||
import com.salesforce.cdp.queryservice.auth.token.CoreToken; | ||
import com.salesforce.cdp.queryservice.auth.token.OffcoreToken; | ||
import com.salesforce.cdp.queryservice.util.TokenException; | ||
import okhttp3.OkHttpClient; | ||
|
||
import java.util.Properties; | ||
|
||
public class TokenManager { | ||
|
||
private final TokenProvider tokenProvider; | ||
|
||
public TokenManager(Properties properties, OkHttpClient client) throws TokenException { | ||
tokenProvider = TokenProviderFactory.getTokenProvider(properties, client); | ||
} | ||
|
||
public TokenManager(TokenProvider tokenProvider) { | ||
this.tokenProvider = tokenProvider; | ||
} | ||
|
||
public CoreToken getCoreToken() throws TokenException { | ||
return tokenProvider.getCoreToken(); | ||
} | ||
|
||
public OffcoreToken getOffcoreToken() throws TokenException { | ||
return tokenProvider.getOffcoreToken(); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/main/java/com/salesforce/cdp/queryservice/auth/TokenProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.salesforce.cdp.queryservice.auth; | ||
|
||
import com.salesforce.cdp.queryservice.auth.token.CoreToken; | ||
import com.salesforce.cdp.queryservice.auth.token.OffcoreToken; | ||
import com.salesforce.cdp.queryservice.util.TokenException; | ||
|
||
public interface TokenProvider { | ||
CoreToken getCoreToken() throws TokenException; | ||
OffcoreToken getOffcoreToken() throws TokenException; | ||
} |
44 changes: 44 additions & 0 deletions
44
src/main/java/com/salesforce/cdp/queryservice/auth/TokenProviderFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.salesforce.cdp.queryservice.auth; | ||
|
||
import com.salesforce.cdp.queryservice.auth.jwt.JwtTokenFlow; | ||
import com.salesforce.cdp.queryservice.auth.jwt.JwtLoginClient; | ||
import com.salesforce.cdp.queryservice.auth.refresh.RefreshTokenFlow; | ||
import com.salesforce.cdp.queryservice.auth.refresh.RefreshTokenClient; | ||
import com.salesforce.cdp.queryservice.auth.unpwd.UnPwdAuthClient; | ||
import com.salesforce.cdp.queryservice.auth.unpwd.UnPwdTokenFlow; | ||
import com.salesforce.cdp.queryservice.util.Constants; | ||
import com.salesforce.cdp.queryservice.util.PropertyUtils; | ||
import com.salesforce.cdp.queryservice.util.TokenException; | ||
import okhttp3.OkHttpClient; | ||
|
||
import java.util.Properties; | ||
|
||
public class TokenProviderFactory { | ||
|
||
public static TokenProvider getTokenProvider(Properties properties, OkHttpClient client) throws TokenException { | ||
TokenExchangeHelper tokenExchangeHelper = new TokenExchangeHelper(properties, client); | ||
if (canSupportUsernamePasswordProvider(properties)) { | ||
return new UnPwdTokenFlow(properties, new UnPwdAuthClient(client), tokenExchangeHelper); | ||
} else if (canSupportJwtProvider(properties)) { | ||
return new JwtTokenFlow(properties, new JwtLoginClient(client), tokenExchangeHelper); | ||
} else if (canSupportRefreshToken(properties)) { | ||
return new RefreshTokenFlow(properties, new RefreshTokenClient(client), | ||
tokenExchangeHelper); | ||
} | ||
throw new TokenException("Sufficient properties for deciding auth flow is not provided"); | ||
} | ||
|
||
private static boolean canSupportUsernamePasswordProvider(Properties properties) { | ||
return PropertyUtils.isPropertyNonEmpty(properties, Constants.USER_NAME) | ||
&& PropertyUtils.isPropertyNonEmpty(properties, Constants.PD); | ||
} | ||
|
||
private static boolean canSupportJwtProvider(Properties properties) { | ||
return PropertyUtils.isPropertyNonEmpty(properties, Constants.USER_NAME) | ||
&& PropertyUtils.isPropertyNonEmpty(properties, Constants.PRIVATE_KEY); | ||
} | ||
|
||
private static boolean canSupportRefreshToken(Properties properties) { | ||
return PropertyUtils.isPropertyNonEmpty(properties, Constants.REFRESHTOKEN); | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
src/main/java/com/salesforce/cdp/queryservice/auth/TokenUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package com.salesforce.cdp.queryservice.auth; | ||
|
||
import com.salesforce.cdp.queryservice.auth.token.CoreToken; | ||
import com.salesforce.cdp.queryservice.auth.token.OffcoreToken; | ||
import com.salesforce.cdp.queryservice.auth.token.Token; | ||
import com.salesforce.cdp.queryservice.util.Constants; | ||
import com.salesforce.cdp.queryservice.util.HttpHelper; | ||
import lombok.extern.slf4j.Slf4j; | ||
import okhttp3.FormBody; | ||
import okhttp3.OkHttpClient; | ||
import okhttp3.Request; | ||
import org.apache.commons.lang3.StringUtils; | ||
|
||
import java.util.*; | ||
|
||
@Slf4j | ||
public class TokenUtils { | ||
|
||
public static boolean isValid(OffcoreToken offcoreToken) { | ||
if (offcoreToken != null) { | ||
Calendar now = Calendar.getInstance(); | ||
return now.compareTo(offcoreToken.getExpireTime()) < 1; | ||
} | ||
return false; | ||
} | ||
|
||
public static void invalidateCoreToken(String url, String coreToken, OkHttpClient client) { | ||
if (coreToken == null) { | ||
return; | ||
} | ||
try { | ||
log.info("Invalidating the core token"); | ||
FormBody formBody = new FormBody.Builder().addEncoded("token", coreToken).build(); | ||
Map<String, String> headers = Collections.singletonMap(Constants.CONTENT_TYPE, Constants.URL_ENCODED_CONTENT); | ||
String tokenRevokeUrl = url + Constants.TOKEN_REVOKE_URL; | ||
Request request = HttpHelper.buildRequest(Constants.POST, tokenRevokeUrl, formBody, headers); | ||
// Response is not needed for this call. | ||
client.newCall(request).execute(); | ||
} catch (Exception e) { | ||
log.error("Revoking the core token failed", e); | ||
} | ||
} | ||
|
||
public static Map<String, String> getTokenWithUrl(Token token) { | ||
Map<String, String> tokenWithUrlMap = new HashMap<>(); | ||
tokenWithUrlMap.put(Constants.ACCESS_TOKEN, token.getTokenType() + StringUtils.SPACE + token.getAccessToken()); | ||
tokenWithUrlMap.put(Constants.TENANT_URL, token.getInstanceUrl()); | ||
return tokenWithUrlMap; | ||
} | ||
|
||
public static void fillArray(byte[] bytes, byte val) { | ||
if (bytes != null) { | ||
Arrays.fill(bytes, val); | ||
} | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
src/main/java/com/salesforce/cdp/queryservice/auth/jwt/JwtLoginClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package com.salesforce.cdp.queryservice.auth.jwt; | ||
|
||
import com.google.common.primitives.Bytes; | ||
import com.salesforce.cdp.queryservice.auth.token.CoreToken; | ||
import com.salesforce.cdp.queryservice.util.Constants; | ||
import com.salesforce.cdp.queryservice.util.HttpHelper; | ||
import com.salesforce.cdp.queryservice.util.TokenException; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import lombok.extern.slf4j.Slf4j; | ||
import okhttp3.*; | ||
|
||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.security.KeyFactory; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.interfaces.RSAPrivateKey; | ||
import java.security.spec.InvalidKeySpecException; | ||
import java.security.spec.PKCS8EncodedKeySpec; | ||
import java.time.Instant; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.*; | ||
|
||
import static com.salesforce.cdp.queryservice.auth.TokenUtils.fillArray; | ||
import static com.salesforce.cdp.queryservice.util.Messages.FAILED_LOGIN; | ||
import static com.salesforce.cdp.queryservice.util.Messages.JWT_CREATION_FAILURE; | ||
|
||
@Slf4j | ||
public class JwtLoginClient { | ||
|
||
private final OkHttpClient client; | ||
|
||
|
||
public JwtLoginClient(OkHttpClient client) { | ||
this.client = client; | ||
} | ||
|
||
public CoreToken keyPairAuthLogin( | ||
String grantType, String clientId, | ||
String userName, String privateKey, String audience, String tokenUrl | ||
) throws TokenException { | ||
byte[] grantTypeSegment = (Constants.GRANT_TYPE_NAME + Constants.TOKEN_ASSIGNMENT + grantType) | ||
.getBytes(StandardCharsets.UTF_8); | ||
byte[] jwsSegment = (Constants.ASSERTION + Constants.TOKEN_ASSIGNMENT + | ||
createJwt(clientId, userName, privateKey, audience)).getBytes(StandardCharsets.UTF_8); | ||
byte[] separator = Constants.TOKEN_SEPARATOR.getBytes(StandardCharsets.UTF_8); | ||
|
||
byte[] body = Bytes.concat( | ||
grantTypeSegment, separator, jwsSegment | ||
); | ||
try { | ||
RequestBody requestBody = RequestBody.create(body, MediaType.parse(Constants.URL_ENCODED_CONTENT)); | ||
Map<String, String> headers = Collections.singletonMap(Constants.CONTENT_TYPE, Constants.URL_ENCODED_CONTENT); | ||
Request request = HttpHelper.buildRequest(Constants.POST, tokenUrl, requestBody, headers); | ||
Response response = client.newCall(request).execute(); | ||
if (!response.isSuccessful()) { | ||
log.error("login with user credentials failed with status code {}", response.code()); | ||
HttpHelper.handleErrorResponse(response, Constants.ERROR_DESCRIPTION); | ||
} | ||
return HttpHelper.handleSuccessResponse(response, CoreToken.class, false); | ||
} catch (IOException e) { | ||
log.error("login with user credentials failed", e); | ||
throw new TokenException(FAILED_LOGIN, e); | ||
} finally { | ||
fillArray(grantTypeSegment, (byte)0); | ||
fillArray(jwsSegment, (byte)0); | ||
fillArray(separator, (byte)0); | ||
fillArray(body, (byte)0); | ||
} | ||
} | ||
|
||
|
||
|
||
private static String createJwt(String clientId, String userName, String privateKey, String audience) throws TokenException { | ||
Instant now = Instant.now(); | ||
String jwtToken = null; | ||
try { | ||
RSAPrivateKey rsaPrivateKey = getPrivateKey(privateKey); | ||
jwtToken = Jwts.builder() | ||
.setIssuer(clientId) | ||
.setSubject(userName) | ||
.setAudience(audience) | ||
.setIssuedAt(Date.from(now)) | ||
.setExpiration(Date.from(now.plus(2l, ChronoUnit.MINUTES))) | ||
.signWith(rsaPrivateKey, SignatureAlgorithm.RS256) | ||
.compact(); | ||
} catch (Exception e) { | ||
log.error("JWT assertion creation failed", e); | ||
throw new TokenException(JWT_CREATION_FAILURE, e); | ||
} | ||
|
||
return jwtToken; | ||
} | ||
|
||
private static RSAPrivateKey getPrivateKey(String rsaPrivateKey) throws NoSuchAlgorithmException, InvalidKeySpecException { | ||
rsaPrivateKey = rsaPrivateKey.replace(Constants.BEGIN_PRIVATE_KEY, ""); | ||
rsaPrivateKey = rsaPrivateKey.replace(Constants.END_PRIVATE_KEY, ""); | ||
rsaPrivateKey = rsaPrivateKey.replaceAll("\\s", ""); | ||
|
||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(rsaPrivateKey)); | ||
KeyFactory kf = KeyFactory.getInstance("RSA"); | ||
RSAPrivateKey privateKey = (RSAPrivateKey)kf.generatePrivate(keySpec); | ||
return privateKey; | ||
} | ||
} |
Oops, something went wrong.