net.accelbyte.sdk.core.AccelByteSDK Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sdk Show documentation
Show all versions of sdk Show documentation
AccelByte Gaming Services Java Extend SDK generated from OpenAPI specs
The newest version!
/*
* Copyright (c) 2022 AccelByte Inc. All Rights Reserved
* This is licensed software from AccelByte Inc, for limitations
* and restrictions contact your company contract manager.
*/
package net.accelbyte.sdk.core;
import static net.accelbyte.sdk.core.AccessTokenPayload.Types.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.math.BigInteger;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.time.Instant;
import java.util.*;
import java.util.Base64.Decoder;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.SneakyThrows;
import lombok.extern.java.Log;
import net.accelbyte.sdk.api.basic.models.NamespaceContext;
import net.accelbyte.sdk.api.basic.operations.namespace.GetNamespaceContext;
import net.accelbyte.sdk.api.basic.wrappers.Namespace;
import net.accelbyte.sdk.api.iam.models.*;
import net.accelbyte.sdk.api.iam.operations.o_auth2_0.AuthorizeV3;
import net.accelbyte.sdk.api.iam.operations.o_auth2_0.AuthorizeV3.CodeChallengeMethod;
import net.accelbyte.sdk.api.iam.operations.o_auth2_0.GetJWKSV3;
import net.accelbyte.sdk.api.iam.operations.o_auth2_0.GetRevocationListV3;
import net.accelbyte.sdk.api.iam.operations.o_auth2_0.PlatformTokenGrantV3;
import net.accelbyte.sdk.api.iam.operations.o_auth2_0.TokenGrantV3;
import net.accelbyte.sdk.api.iam.operations.o_auth2_0.VerifyTokenV3;
import net.accelbyte.sdk.api.iam.operations.o_auth2_0_extension.UserAuthenticationV3;
import net.accelbyte.sdk.api.iam.operations.roles.AdminGetRoleV3;
import net.accelbyte.sdk.api.iam.wrappers.OAuth20;
import net.accelbyte.sdk.api.iam.wrappers.OAuth20Extension;
import net.accelbyte.sdk.api.iam.wrappers.Roles;
import net.accelbyte.sdk.core.client.HttpClient;
import net.accelbyte.sdk.core.repository.*;
import net.accelbyte.sdk.core.util.BloomFilter;
import net.accelbyte.sdk.core.util.Helper;
import net.accelbyte.sdk.core.validator.RoleCacheKey;
import net.accelbyte.sdk.core.validator.UserAuthContext;
import okhttp3.Credentials;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
@Log
public class AccelByteSDK {
private static final String COOKIE_KEY_ACCESS_TOKEN = "access_token";
private static final String DEFAULT_LOGIN_USER_SCOPE =
"commerce account social publishing analytics";
private static final String DEFAULT_CACHE_KEY = "default";
private static final String CLAIM_SUB = "sub";
private AccelByteConfig sdkConfiguration;
private final Timer refreshTokenTimer = new Timer("RefreshTokenTimer", true);
private final Object refreshTokenTaskLock = new Object();
private TimerTask refreshTokenTask = null;
private final ReentrantLock refreshTokenMethodLock = new ReentrantLock();
private LoadingCache> jwksCache;
private LoadingCache revocationListCache;
private LoadingCache namespaceContextCache;
private LoadingCache> rolePermissionsCache;
private static final BloomFilter bloomFilter = new BloomFilter();
// TODO: make this configurable
private float tokenRefreshRatio = 0.8f;
private ObjectMapper objectMapper = new ObjectMapper();
protected boolean internalValidateToken(
SignedJWT signedJWT, String token, String resource, int action) {
final UserAuthContext authContext = UserAuthContext.builder().token(token).build();
final Permission permission = Permission.builder().resource(resource).action(action).build();
return internalValidateToken(signedJWT, authContext, permission);
}
protected boolean internalValidateToken(
SignedJWT signedJWT, UserAuthContext authContext, Permission permission) {
try {
final JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
final boolean isLocalTokenValidationEnabled =
jwksCache != null && revocationListCache != null;
if (isLocalTokenValidationEnabled) {
final String kid = signedJWT.getHeader().getKeyID();
final RSAPublicKey pubKey = jwksCache.get(DEFAULT_CACHE_KEY).get(kid);
if (pubKey == null) {
return false; // Matching JWK key not found
}
final JWSVerifier verifier = new RSASSAVerifier(pubKey);
if (!signedJWT.verify(verifier)) {
return false; // JWT signature verification failed
}
if (jwtClaimsSet.getExpirationTime() == null
|| jwtClaimsSet.getExpirationTime().before(new Date())) {
return false; // JWT expired
}
final OauthapiRevocationList revocationList = revocationListCache.get(DEFAULT_CACHE_KEY);
final BloomFilterJSON revokedTokens = revocationList.getRevokedTokens();
final long[] bits =
revokedTokens.getBits().stream().mapToLong(BigInteger::longValue).toArray();
final int k = revokedTokens.getK();
final int m = revokedTokens.getM();
final boolean isTokenRevoked =
bloomFilter.mightContain(authContext.getToken(), k, BitSet.valueOf(bits), m);
if (isTokenRevoked) {
return false;
}
final String tokenUserId = (String) jwtClaimsSet.getClaim(CLAIM_SUB);
if (tokenUserId != null && !tokenUserId.equals("")) {
final boolean isUserRevoked =
revocationList.getRevokedUsers().stream()
.anyMatch(ruid -> tokenUserId.equals(ruid.getId()));
if (isUserRevoked) {
return false;
}
}
} else {
final OAuth20 oAuth20 = new OAuth20(this);
oAuth20.verifyTokenV3(VerifyTokenV3.builder().token(authContext.getToken()).build());
}
if (Strings.isNullOrEmpty(permission.getResource())) {
return true; // Check token only without checking resource
}
final AccessTokenPayload accessTokenPayload =
objectMapper.convertValue(jwtClaimsSet.toJSONObject(), AccessTokenPayload.class);
return hasValidPermission(accessTokenPayload, authContext, permission);
} catch (Exception e) {
log.warning(e.getMessage());
}
return false;
}
private boolean hasValidPermission(
AccessTokenPayload tokenPayload, UserAuthContext authContext, Permission permission) {
if (permission == null) {
return true;
}
if (Strings.isNullOrEmpty(tokenPayload.getNamespace())) {
return false;
}
String tokenNamespace = tokenPayload.getNamespace();
String expandedResource =
expandResource(
permission.getResource(), authContext.getNamespace(), authContext.getUserId());
List originPermissions = tokenPayload.getPermissions();
if (validatePermission(originPermissions, expandedResource, permission.getAction())) {
return true;
}
String claimsUserId = tokenPayload.getSub();
List namespaceRoles = tokenPayload.getNamespaceRoles();
if (!Strings.isNullOrEmpty(claimsUserId) && !namespaceRoles.isEmpty()) {
List allRoleNamespacePermissions =
namespaceRoles.stream()
.map(
it -> {
try {
RoleCacheKey key = RoleCacheKey.of(it, claimsUserId);
return rolePermissionsCache.get(key);
} catch (ExecutionException e) {
log.warning(e.getMessage());
return null;
}
})
.filter(Objects::nonNull)
.flatMap(List::stream)
.collect(Collectors.toList());
return !allRoleNamespacePermissions.isEmpty()
&& validatePermission(
allRoleNamespacePermissions, expandedResource, permission.getAction());
}
List claimRoles = tokenPayload.getRoles();
if (claimRoles != null && !claimRoles.isEmpty()) {
List allRolePermissions =
claimRoles.stream()
.map(
it -> {
try {
RoleCacheKey key =
RoleCacheKey.of(it, tokenNamespace, authContext.getUserId());
return rolePermissionsCache.get(key);
} catch (ExecutionException e) {
log.warning(e.getMessage());
return null;
}
})
.filter(Objects::nonNull)
.flatMap(List::stream)
.collect(Collectors.toList());
return !allRolePermissions.isEmpty()
&& validatePermission(allRolePermissions, expandedResource, permission.getAction());
}
return false;
}
private boolean validatePermission(
List ownedPermissions, String requestedResource, int requestedAction) {
if (ownedPermissions == null) {
return false;
}
if (ownedPermissions.isEmpty()) {
return false;
}
String[] requestedResourceElem = requestedResource.trim().split(":");
for (Permission ownedPermission : ownedPermissions) {
String[] ownedResourceElem = ownedPermission.getResource().split(":");
if (ownedResourceElem.length == 0) {
continue;
}
int minResLen = Math.min(ownedResourceElem.length, requestedResourceElem.length);
boolean isResMatches =
IntStream.range(0, minResLen)
.allMatch(i -> isResourceElementMatch(i, ownedResourceElem, requestedResourceElem));
if (!isResMatches) {
continue;
}
if (isResourceMatch(
ownedResourceElem, requestedResourceElem, ownedPermission.getAction(), requestedAction)) {
return true;
}
}
return false;
}
private boolean isResourceMatch(
String[] ownedResourceElem,
String[] requestedResourceElem,
int ownedAction,
int requestedAction) {
int ownedLen = ownedResourceElem.length;
int requestedLen = requestedResourceElem.length;
boolean matches = true;
if (ownedLen < requestedLen) {
matches = handleShorterRequestedResource(ownedResourceElem, ownedLen);
} else {
matches = handleLongerRequestedResource(ownedResourceElem, ownedLen, requestedLen);
}
if (!matches) {
return false;
}
return (ownedAction & requestedAction) > 0;
}
private boolean handleLongerRequestedResource(String[] ownedResourceElem, int start, int end) {
for (int i = start; i < end; i++) {
if (!ownedResourceElem[i].equals("*")) {
return false;
}
}
return true;
}
private boolean handleShorterRequestedResource(String[] ownedResourceElem, int ownedLen) {
if (ownedResourceElem[ownedLen - 1].equals("*")) {
if (ownedLen < 2) {
return true;
}
String segment = ownedResourceElem[ownedLen - 2];
return !segment.equals("NAMESPACE") && !segment.equals("USER");
}
return false;
}
private boolean isResourceElementMatch(
int index, String[] ownedResourceElem, String[] requestedResourceElem) {
String ownElem = ownedResourceElem[index];
String reqElem = requestedResourceElem[index];
if (!ownElem.equals(reqElem) && !ownElem.equals("*")) {
if (index > 0 && ownElem.endsWith("-")) {
String prevOwnElem = ownedResourceElem[index - 1];
if (prevOwnElem.endsWith("NAMESPACE")) {
if (reqElem.contains("-")
&& reqElem.split("-").length == 2
&& reqElem.startsWith(ownElem)) {
return true;
}
if (reqElem.equals(ownElem + "-")) {
return true;
}
NamespaceContext namespaceContext = null;
try {
if (namespaceContextCache != null) {
namespaceContext = namespaceContextCache.get(reqElem);
}
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
if (namespaceContext != null
&& namespaceContext.getType().equals("Game")
&& reqElem.startsWith(namespaceContext.getStudioNamespace())) {
return true;
}
}
}
return false;
}
return true;
}
private String expandResource(String resource, String namespace, String userId) {
String expandedResource = resource;
if (!Strings.isNullOrEmpty(namespace)) {
expandedResource = expandedResource.replace("{namespace}", namespace);
}
if (!Strings.isNullOrEmpty(userId)) {
expandedResource = expandedResource.replace("{userId}", userId);
}
return expandedResource;
}
public AccelByteSDK(
HttpClient> httpClient,
TokenRepository tokenRepository,
ConfigRepository configRepository) {
this(httpClient, tokenRepository, configRepository, FlightIdRepository.getInstance());
}
AccelByteSDK(
HttpClient> httpClient,
TokenRepository tokenRepository,
ConfigRepository configRepository,
FlightIdRepository flightIdRepository) {
this(new AccelByteConfig(httpClient, tokenRepository, configRepository, flightIdRepository));
}
public AccelByteSDK(AccelByteConfig sdkConfiguration) {
this.sdkConfiguration = sdkConfiguration;
if (this.sdkConfiguration.getConfigRepository() instanceof TokenValidation) {
final TokenValidation tokenValidation =
(TokenValidation) this.sdkConfiguration.getConfigRepository();
this.namespaceContextCache =
buildNamespaceContextCache(this, tokenValidation.getJwksRefreshInterval());
if (tokenValidation.getLocalTokenValidationEnabled()) {
this.jwksCache = buildJWKSLoadingCache(this, tokenValidation.getJwksRefreshInterval());
this.revocationListCache =
buildRevocationListLoadingCache(
this, tokenValidation.getRevocationListRefreshInterval());
try {
// ensure the cache is ready, to prevent concurrent request being blocked when cache not
// yet initialized
this.jwksCache.get(DEFAULT_CACHE_KEY);
this.revocationListCache.get(DEFAULT_CACHE_KEY);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
this.rolePermissionsCache = buildRolePermissionLoadingCache(this);
}
}
public AccelByteConfig getSdkConfiguration() {
return sdkConfiguration;
}
public HttpResponse runRequest(Operation operation) throws Exception {
String selectedSecurity = Operation.Security.Basic.toString();
if (!operation.getPreferredSecurityMethod().isEmpty())
selectedSecurity = operation.getPreferredSecurityMethod();
else if (operation.getSecurities().size() > 0) {
selectedSecurity = operation.getSecurities().get(0);
}
final HttpHeaders headers = new HttpHeaders();
final Map cookies = operation.getCookieParams();
final ConfigRepository configRepository = sdkConfiguration.getConfigRepository();
final String accessToken = sdkConfiguration.getTokenRepository().getToken();
if (Operation.Security.Basic.toString().equals(selectedSecurity)) {
final String clientId = configRepository.getClientId();
final String clientSecret = configRepository.getClientSecret();
headers.put(HttpHeaders.AUTHORIZATION, Credentials.basic(clientId, clientSecret));
} else if (Operation.Security.Bearer.toString().equals(selectedSecurity)) {
if (accessToken != null && !accessToken.isEmpty()) {
headers.put(
HttpHeaders.AUTHORIZATION, Operation.Security.Bearer.toString() + " " + accessToken);
}
} else if (Operation.Security.Cookie.toString().equals(selectedSecurity)) {
if (accessToken != null && !accessToken.isEmpty()) {
cookies.put(COOKIE_KEY_ACCESS_TOKEN, accessToken);
}
}
if (configRepository.isAmazonTraceId()) {
final String version = configRepository.getAmazonTraceIdVersion();
headers.put(HttpHeaders.X_AMZN_TRACE_ID, Helper.generateAmazonTraceId(version));
}
final FlightIdRepository flightIdRepository = sdkConfiguration.getFlightIdRepository();
if (configRepository.isFlightIdEnabled()) {
if (operation.hasXFlightId()) {
headers.put(HttpHeaders.X_FLIGHT_ID, operation.getXFlightId());
} else {
headers.put(HttpHeaders.X_FLIGHT_ID, flightIdRepository.getFlightId());
}
}
if (configRepository.isClientInfoHeader()) {
final String sdkName = SDKInfo.getInstance().getSdkName();
final String sdkVersion = SDKInfo.getInstance().getSdkVersion();
final AppInfo appInfo = configRepository.getAppInfo();
final String appName = appInfo.getAppName();
final String appVersion = appInfo.getAppVersion();
final String userAgent =
String.format("%s/%s (%s/%s)", sdkName, sdkVersion, appName, appVersion);
headers.put(HttpHeaders.USER_AGENT, userAgent);
}
if (cookies.size() > 0) {
final List cookieEntries = new ArrayList();
for (Map.Entry key : cookies.entrySet()) {
cookieEntries.add(
URLEncoder.encode(key.getKey(), StandardCharsets.UTF_8.toString())
+ "="
+ URLEncoder.encode(key.getValue(), StandardCharsets.UTF_8.toString()));
}
headers.put(HttpHeaders.COOKIE, String.join("; ", cookieEntries));
}
final String baseUrl = sdkConfiguration.getConfigRepository().getBaseURL();
return sdkConfiguration.getHttpClient().sendRequest(operation, baseUrl, headers);
}
public boolean loginUser(String username, String password) {
return loginUser(username, password, DEFAULT_LOGIN_USER_SCOPE);
}
public boolean loginUser(String username, String password, String scope) {
final String codeVerifier = Helper.generateCodeVerifier();
final String codeChallenge = Helper.generateCodeChallenge(codeVerifier);
final String clientId = this.sdkConfiguration.getConfigRepository().getClientId();
try {
final OAuth20 oAuth20 = new OAuth20(this);
final OAuth20Extension oAuth20Extension = new OAuth20Extension(this);
final AuthorizeV3 authorizeV3 =
AuthorizeV3.builder()
.codeChallenge(codeChallenge)
.codeChallengeMethodFromEnum(CodeChallengeMethod.S256)
.scope(scope)
.clientId(clientId)
.responseTypeFromEnum(AuthorizeV3.ResponseType.Code)
.build();
final String authorizeResponse = oAuth20.authorizeV3(authorizeV3);
final List authorizeParams =
URLEncodedUtils.parse(new URI(authorizeResponse), StandardCharsets.UTF_8);
final String requestId =
authorizeParams.stream()
.filter(
(q) -> {
return q.getName().equals(authorizeV3.getLocationQuery());
})
.findFirst()
.map(NameValuePair::getValue)
.orElse(null);
final UserAuthenticationV3 userAuthenticationV3 =
UserAuthenticationV3.builder()
.clientId(clientId)
.userName(username)
.password(password)
.requestId(requestId)
.build();
final String authenticationResponse =
oAuth20Extension.userAuthenticationV3(userAuthenticationV3);
final List authenticationParams =
URLEncodedUtils.parse(new URI(authenticationResponse), StandardCharsets.UTF_8);
final String code =
authenticationParams.stream()
.filter(
(q) -> {
return q.getName().equals(userAuthenticationV3.getLocationQuery());
})
.findFirst()
.map(NameValuePair::getValue)
.orElse(null);
if (code == null) {
return false; // Invalid username or password?
}
final Instant utcNow = Instant.now();
final TokenGrantV3 tokenGrantV3 =
TokenGrantV3.builder()
.clientId(clientId)
.code(code)
.codeVerifier(codeVerifier)
.grantTypeFromEnum(TokenGrantV3.GrantType.AuthorizationCode)
.build();
final OauthmodelTokenWithDeviceCookieResponseV3 token = oAuth20.tokenGrantV3(tokenGrantV3);
final TokenRepository tokenRepository = this.sdkConfiguration.getTokenRepository();
tokenRepository.storeToken(token.getAccessToken());
if (tokenRepository instanceof TokenRefresh) {
final TokenRefresh tokenRefresh = (TokenRefresh) tokenRepository;
final long expiresIn = (long) (token.getExpiresIn() * tokenRefreshRatio);
final long refreshExpiresIn = (long) (token.getRefreshExpiresIn() * tokenRefreshRatio);
tokenRefresh.setTokenExpiresAt(Date.from(utcNow.plusSeconds(expiresIn)));
tokenRefresh.storeRefreshToken(token.getRefreshToken());
tokenRefresh.setRefreshTokenExpiresAt(Date.from(utcNow.plusSeconds(refreshExpiresIn)));
scheduleRefreshTokenTask(expiresIn);
}
return true;
} catch (Exception e) {
log.warning(e.getMessage());
}
return false;
}
public boolean loginClient() {
try {
final OAuth20 oAuth20 = new OAuth20(this);
final Instant utcNow = Instant.now();
final TokenGrantV3 tokenGrantV3 =
TokenGrantV3.builder()
.grantTypeFromEnum(TokenGrantV3.GrantType.ClientCredentials)
.build();
final OauthmodelTokenWithDeviceCookieResponseV3 token = oAuth20.tokenGrantV3(tokenGrantV3);
final TokenRepository tokenRepository = this.sdkConfiguration.getTokenRepository();
tokenRepository.storeToken(token.getAccessToken());
if (tokenRepository instanceof TokenRefresh) {
final TokenRefresh tokenRefresh = (TokenRefresh) tokenRepository;
final long expiresIn = (long) (token.getExpiresIn() * tokenRefreshRatio);
tokenRefresh.setTokenExpiresAt(Date.from(utcNow.plusSeconds(expiresIn)));
tokenRefresh.storeRefreshToken(null);
tokenRefresh.setRefreshTokenExpiresAt(null);
scheduleRefreshTokenTask(expiresIn);
}
return true;
} catch (Exception e) {
log.warning(e.getMessage());
}
return false;
}
@SneakyThrows // TODO: remove unused exception from getToken, getTokenExpiredAt
public boolean loginOrRefreshClient() {
TokenRepository tokenRepo = sdkConfiguration.getTokenRepository();
TokenRefresh refreshRepo;
if (tokenRepo instanceof TokenRefresh) {
refreshRepo = (TokenRefresh) tokenRepo;
} else {
throw new IllegalArgumentException(
"Token repository is not a Refresh Repository"); // TODO: restructure the inheritance
}
if (Strings.isNullOrEmpty(tokenRepo.getToken())) {
return loginClient();
}
boolean isAccessTokenExpired = isExpired(refreshRepo.getTokenExpiresAt());
if (!isAccessTokenExpired) {
return true; // do nothing, since accessToken still valid
}
return loginClient();
}
@SneakyThrows // TODO: remove unused exception from getToken, getTokenExpiredAt
public boolean loginOrRefreshUser(String username, String password) {
TokenRepository tokenRepo = sdkConfiguration.getTokenRepository();
TokenRefresh refreshRepo;
if (tokenRepo instanceof TokenRefresh) {
refreshRepo = (TokenRefresh) tokenRepo;
} else {
throw new IllegalArgumentException(
"Token repository is not a Refresh Repository"); // TODO: restructure the inheritance
}
if (Strings.isNullOrEmpty(tokenRepo.getToken())) {
return loginUser(username, password);
}
boolean isAccessTokenExpired = isExpired(refreshRepo.getTokenExpiresAt());
boolean isRefreshTokenExpired = isExpired(refreshRepo.getRefreshTokenExpiresAt());
if (!isAccessTokenExpired) {
return true; // do nothing, since accessToken still valid
}
if (!isRefreshTokenExpired) {
return refreshToken();
}
return loginUser(username, password);
}
public boolean loginPlatform(String platformId, String platformToken) {
try {
final OAuth20 oAuth20 = new OAuth20(this);
final Instant utcNow = Instant.now();
final PlatformTokenGrantV3 tokenGrantV3 =
PlatformTokenGrantV3.builder()
.platformId(platformId)
.platformToken(platformToken)
.build();
final OauthmodelTokenResponse token = oAuth20.platformTokenGrantV3(tokenGrantV3);
final TokenRepository tokenRepository = this.sdkConfiguration.getTokenRepository();
tokenRepository.storeToken(token.getAccessToken());
if (tokenRepository instanceof TokenRefresh) {
final TokenRefresh tokenRefresh = (TokenRefresh) tokenRepository;
final long expiresIn = (long) (token.getExpiresIn() * tokenRefreshRatio);
final long refreshExpiresIn = (long) (token.getRefreshExpiresIn() * tokenRefreshRatio);
tokenRefresh.setTokenExpiresAt(Date.from(utcNow.plusSeconds(expiresIn)));
tokenRefresh.storeRefreshToken(token.getRefreshToken());
tokenRefresh.setRefreshTokenExpiresAt(Date.from(utcNow.plusSeconds(refreshExpiresIn)));
scheduleRefreshTokenTask(expiresIn);
}
return true;
} catch (Exception e) {
log.warning(e.getMessage());
}
return false;
}
/**
* Attempts to perform the refresh token operation with a default wait time of 500 milliseconds to
* acquire the necessary lock. Will return false if 500 milliseconds of waiting passed. Refer to
* {@link #refreshToken(long, TimeUnit)} for customized timeout.
* WARNING: Please don't use this method if you use TokenRepository class with
* TokenRefreshRepository interface a.k.a. automatic refresh token enabled.
*
* @return {@code true} if operation was successful, {@code false} otherwise.
*/
public boolean refreshToken() {
return refreshToken(500, TimeUnit.MILLISECONDS);
}
public boolean refreshToken(long timeout, TimeUnit unit) {
boolean acquiredLock = false;
try {
acquiredLock = refreshTokenMethodLock.tryLock(timeout, unit);
if (!acquiredLock) {
log.warning(String.format("unable to acquire lock after (%s)%s", timeout, unit));
return false; // Refresh token in-progress
}
final TokenRepository tokenRepository = sdkConfiguration.getTokenRepository();
final String accessToken = tokenRepository.getToken();
if (accessToken == null || accessToken.isEmpty()) {
return false; // Cannot perform token refresh
}
if (!(tokenRepository instanceof TokenRefresh)) {
return false; // Cannot perform token refresh
}
final TokenRefresh tokenRefresh = (TokenRefresh) tokenRepository;
final Date accessTokenExpiresAt = tokenRefresh.getTokenExpiresAt();
final String refreshToken = tokenRefresh.getRefreshToken();
final boolean isLoginUserOrLoginPlatform = refreshToken != null && !refreshToken.isEmpty();
final Date refreshTokenExpiresAt =
isLoginUserOrLoginPlatform ? tokenRefresh.getRefreshTokenExpiresAt() : null;
if (accessTokenExpiresAt == null) {
return false; // Cannot perform token refresh
}
if (isLoginUserOrLoginPlatform) {
final boolean isRefreshTokenExpired = isExpired(refreshTokenExpiresAt);
if (isRefreshTokenExpired) {
return false; // Cannot perform token refresh
}
try {
final Instant utcNow = Instant.now();
final OAuth20 oAuth20 = new OAuth20(this);
final TokenGrantV3 tokenGrantV3 =
TokenGrantV3.builder()
.refreshToken(refreshToken)
.grantTypeFromEnum(TokenGrantV3.GrantType.RefreshToken)
.build();
final OauthmodelTokenWithDeviceCookieResponseV3 token =
oAuth20.tokenGrantV3(tokenGrantV3);
final long expiresIn = (long) (token.getExpiresIn() * tokenRefreshRatio);
final long refreshExpiresIn = (long) (token.getRefreshExpiresIn() * tokenRefreshRatio);
tokenRepository.storeToken(token.getAccessToken());
tokenRefresh.setTokenExpiresAt(Date.from(utcNow.plusSeconds(expiresIn)));
tokenRefresh.storeRefreshToken(token.getRefreshToken());
tokenRefresh.setRefreshTokenExpiresAt(Date.from(utcNow.plusSeconds(refreshExpiresIn)));
scheduleRefreshTokenTask(expiresIn);
return true; // Token refresh successful
} catch (Exception e) {
log.warning(e.getMessage());
}
return false; // Token refresh failed
} else {
final boolean isLoginClientOk = this.loginClient();
return isLoginClientOk; // Token refresh successful or failed
}
} catch (Exception e) {
log.warning(e.getMessage());
} finally {
// to ensure, when in a race condition (i.e. this method called by multiple thread at the same
// time)
// and lock haven't been acquired by any thread yet.
// adding this will ensure only the owner can unlock, to prevent error IllegalMonitoringState
if (acquiredLock) {
refreshTokenMethodLock.unlock();
}
}
return false;
}
public boolean validateToken(String token) {
return validateToken(token, null, 0);
}
public boolean validateToken(String token, String resource, int action) {
try {
final SignedJWT signedJWT = SignedJWT.parse(token);
return internalValidateToken(signedJWT, token, resource, action);
} catch (Exception e) {
log.warning(e.getMessage());
}
return false;
}
/** Validating user token in authContext against the required permission */
public boolean validateToken(UserAuthContext authContext, Permission permission) {
try {
final SignedJWT signedJWT = SignedJWT.parse(authContext.getToken());
return internalValidateToken(signedJWT, authContext, permission);
} catch (Exception e) {
log.warning(e.getMessage());
}
return false;
}
public AccessTokenPayload parseAccessToken(String token, Boolean validateFirst) {
try {
final SignedJWT signedJWT = SignedJWT.parse(token);
if (validateFirst) {
final boolean isValid = internalValidateToken(signedJWT, token, null, 0);
if (!isValid) return null;
}
final String payloadStr = signedJWT.getPayload().toString();
return new AccessTokenPayload().createFromJson(payloadStr);
} catch (Exception e) {
log.warning(e.getMessage());
}
return null;
}
public boolean logout() {
try {
final TokenRepository tokenRepository = this.sdkConfiguration.getTokenRepository();
tokenRepository.removeToken();
if (tokenRepository instanceof TokenRefresh) {
final TokenRefresh tokenRefresh = (TokenRefresh) tokenRepository;
tokenRefresh.setTokenExpiresAt(null);
tokenRefresh.removeRefreshToken();
tokenRefresh.setRefreshTokenExpiresAt(null);
cancelRefreshTokenTask();
}
return true;
} catch (Exception e) {
log.warning(e.getMessage());
}
return false;
}
private void scheduleRefreshTokenTask(long delaySeconds) {
synchronized (refreshTokenTaskLock) {
if (refreshTokenTask != null) {
refreshTokenTask.cancel();
}
refreshTokenTask =
new TimerTask() {
public void run() {
final boolean isRefreshTokenOk = refreshToken();
if (!isRefreshTokenOk) {
scheduleRefreshTokenTask(10);
}
}
};
refreshTokenTimer.schedule(refreshTokenTask, delaySeconds * 1000);
}
}
private void cancelRefreshTokenTask() {
synchronized (refreshTokenTaskLock) {
if (refreshTokenTask != null) {
refreshTokenTask.cancel();
}
}
}
private static boolean isExpired(Date expiresAt) {
final long tokenExpiresAtEpoch = expiresAt.getTime();
final long utcNowEpoch = Date.from(Instant.now()).getTime();
final boolean isExpired = (tokenExpiresAtEpoch - utcNowEpoch) <= 0;
return isExpired;
}
private LoadingCache> buildRolePermissionLoadingCache(
AccelByteSDK sdk) {
final CacheLoader> rolePermissionLoader =
new CacheLoader>() {
@Override
public List load(RoleCacheKey key) throws Exception {
final Roles rolesWrapper = new Roles(sdk);
final AdminGetRoleV3 param = AdminGetRoleV3.builder().roleId(key.getRoleId()).build();
final ModelRoleResponseV3 getRoleV3Result = rolesWrapper.adminGetRoleV3(param);
// go ref: getRolePermission
List permissions =
getRoleV3Result.getPermissions().stream()
.map(Permission::of)
.collect(Collectors.toList());
// go ref: getRolePermission2
permissions =
permissions.stream()
.peek(
it -> {
String expandedPermission =
expandResource(it.getResource(), key.getNamespace(), key.getUserId());
it.setResource(expandedPermission);
})
.collect(Collectors.toList());
return permissions;
}
};
// TODO: make this configurable if needed, currently the cache will have 5min TTL
int rolePermissionRefreshIntervalSeconds = 300;
return CacheBuilder.newBuilder()
.refreshAfterWrite(rolePermissionRefreshIntervalSeconds, TimeUnit.SECONDS)
.build(rolePermissionLoader);
}
private LoadingCache> buildJWKSLoadingCache(
AccelByteSDK sdk, int refreshIntervalSeconds) {
final CacheLoader> jwksLoader =
new CacheLoader>() {
@Override
public Map load(String key) throws Exception {
final OAuth20 oauthWrapper = new OAuth20(sdk);
final OauthcommonJWKSet getJwksV3Result =
oauthWrapper.getJWKSV3(GetJWKSV3.builder().build());
final Decoder urlDecoder = Base64.getUrlDecoder();
final KeyFactory rsaKeyFactory = KeyFactory.getInstance("RSA");
final Map result =
getJwksV3Result.getKeys().stream()
.collect(
Collectors.toMap(
OauthcommonJWKKey::getKid,
jwkKey -> {
try {
final BigInteger modulus =
new BigInteger(1, urlDecoder.decode(jwkKey.getN()));
final BigInteger exponent =
new BigInteger(1, urlDecoder.decode(jwkKey.getE()));
final RSAPublicKeySpec rsaPubKeySpec =
new RSAPublicKeySpec(modulus, exponent);
final RSAPublicKey pubKey =
(RSAPublicKey) rsaKeyFactory.generatePublic(rsaPubKeySpec);
return pubKey;
} catch (InvalidKeySpecException e) {
log.warning(e.getMessage());
return null;
}
}));
return result;
}
};
return CacheBuilder.newBuilder()
.refreshAfterWrite(refreshIntervalSeconds, TimeUnit.SECONDS)
.build(jwksLoader);
}
private LoadingCache buildRevocationListLoadingCache(
AccelByteSDK sdk, int refreshIntervalSeconds) {
final CacheLoader revocationLoader =
new CacheLoader() {
@Override
public OauthapiRevocationList load(String key) throws Exception {
final OAuth20 oauthWrapper = new OAuth20(sdk);
final OauthapiRevocationList getRevocationListV3Result =
oauthWrapper.getRevocationListV3(GetRevocationListV3.builder().build());
return getRevocationListV3Result;
}
};
return CacheBuilder.newBuilder()
.refreshAfterWrite(refreshIntervalSeconds, TimeUnit.SECONDS)
.build(revocationLoader);
}
// TODO figure out how to decouple IAM + basic
// because now IAM depends on module basic
private LoadingCache buildNamespaceContextCache(
AccelByteSDK sdk, int refreshIntervalSeconds) {
final CacheLoader revocationLoader =
new CacheLoader() {
@Override
public NamespaceContext load(String key) throws Exception {
final Namespace namespaceWrapper = new Namespace(sdk);
final NamespaceContext namespaceContext =
namespaceWrapper.getNamespaceContext(
GetNamespaceContext.builder().namespace(key).build());
return namespaceContext;
}
};
return CacheBuilder.newBuilder()
.refreshAfterWrite(refreshIntervalSeconds, TimeUnit.SECONDS)
.build(revocationLoader);
}
}