diff --git a/src/main/java/com/aliyun/credentials/provider/CLIProfileCredentialsProvider.java b/src/main/java/com/aliyun/credentials/provider/CLIProfileCredentialsProvider.java index e350e93..ef76113 100644 --- a/src/main/java/com/aliyun/credentials/provider/CLIProfileCredentialsProvider.java +++ b/src/main/java/com/aliyun/credentials/provider/CLIProfileCredentialsProvider.java @@ -8,14 +8,26 @@ import com.aliyun.credentials.utils.StringUtils; import com.aliyun.tea.utils.Validate; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.annotations.SerializedName; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; +import java.io.*; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class CLIProfileCredentialsProvider implements AlibabaCloudCredentialsProvider { + private static final Map OAUTH_BASE_URL_MAP = new HashMap() {{ + put("CN", "https://oauth.aliyun.com"); + put("INTL", "https://oauth.alibabacloud.com"); + }}; + private static final Map OAUTH_CLIENT_MAP = new HashMap() {{ + put("CN", "4038181954557748008"); + put("INTL", "4103531455503354461"); + }}; + private final String CLI_CREDENTIALS_CONFIG_PATH = System.getProperty("user.home") + "/.aliyun/config.json"; private volatile AlibabaCloudCredentialsProvider credentialsProvider; @@ -142,6 +154,30 @@ AlibabaCloudCredentialsProvider reloadCredentialsProvider(Config config, String .policy(profile.getPolicy()) .externalId(profile.getExternalId()) .build(); + case "CloudSSO": + return CloudSSOCredentialsProvider.builder() + .signInUrl(profile.getSignInUrl()) + .accountId(profile.getAccountId()) + .accessConfig(profile.getAccessConfig()) + .accessToken(profile.getAccessToken()) + .accessTokenExpire(profile.getAccessTokenExpire()) + .build(); + case "OAuth": + String siteType = profile.getOauthSiteType() != null + ? profile.getOauthSiteType().toUpperCase() : ""; + String oauthSignInUrl = OAUTH_BASE_URL_MAP.get(siteType); + if (StringUtils.isEmpty(oauthSignInUrl)) { + throw new CredentialException("Invalid OAuth site type, support CN or INTL."); + } + String oauthClientId = OAUTH_CLIENT_MAP.get(siteType); + return OAuthCredentialsProvider.builder() + .signInUrl(oauthSignInUrl) + .clientId(oauthClientId) + .refreshToken(profile.getOauthRefreshToken()) + .accessToken(profile.getOauthAccessToken()) + .accessTokenExpire(profile.getOauthAccessTokenExpire()) + .tokenUpdateCallback(createOAuthTokenUpdateCallback()) + .build(); default: throw new CredentialException(String.format("Unsupported profile mode '%s' form CLI credentials file.", profile.getMode())); } @@ -183,6 +219,81 @@ public String getProviderName() { return ProviderName.CLI_PROFILE; } + private OAuthCredentialsProvider.OAuthTokenUpdateCallback createOAuthTokenUpdateCallback() { + return (refreshToken, accessToken, accessKeyId, accessKeySecret, securityToken, accessTokenExpire, stsExpire) -> { + updateOAuthTokens(refreshToken, accessToken, accessKeyId, accessKeySecret, securityToken, accessTokenExpire, stsExpire); + }; + } + + private void updateOAuthTokens(String refreshToken, String accessToken, String accessKeyId, + String accessKeySecret, String securityToken, + long accessTokenExpire, long stsExpire) { + File configFile = new File(CLI_CREDENTIALS_CONFIG_PATH); + if (!configFile.exists()) { + return; + } + + try (RandomAccessFile raf = new RandomAccessFile(configFile, "rw"); + FileChannel channel = raf.getChannel(); + FileLock lock = channel.lock()) { + + byte[] bytes = new byte[(int) raf.length()]; + raf.readFully(bytes); + String jsonContent = new String(bytes, "UTF-8"); + + Gson gson = new Gson(); + Config config = gson.fromJson(jsonContent, Config.class); + if (config == null || config.getProfiles() == null) { + return; + } + + String profileName = this.currentProfileName; + if (StringUtils.isEmpty(profileName)) { + profileName = config.getCurrent(); + } + + Profile oauthProfile = findOAuthProfile(config, profileName); + if (oauthProfile == null) { + return; + } + + oauthProfile.setOauthRefreshToken(refreshToken); + oauthProfile.setOauthAccessToken(accessToken); + oauthProfile.setOauthAccessTokenExpire(accessTokenExpire); + oauthProfile.setAccessKeyId(accessKeyId); + oauthProfile.setAccessKeySecret(accessKeySecret); + oauthProfile.setSecurityToken(securityToken); + oauthProfile.setStsExpire(stsExpire); + + Gson writer = new GsonBuilder().setPrettyPrinting().create(); + String updatedJson = writer.toJson(config); + + raf.seek(0); + raf.setLength(0); + raf.write(updatedJson.getBytes("UTF-8")); + } catch (Exception e) { + // Warning only + } + } + + private Profile findOAuthProfile(Config config, String profileName) { + if (config.getProfiles() == null) { + return null; + } + for (Profile p : config.getProfiles()) { + if (p.getName() != null && p.getName().equals(profileName)) { + if ("OAuth".equals(p.getMode())) { + return p; + } + if (!StringUtils.isEmpty(p.getSourceProfile())) { + return findOAuthProfile(config, p.getSourceProfile()); + } + return null; + } + } + return null; + } + @Override public void close() { } @@ -248,6 +359,26 @@ static class Profile { private String policy; @SerializedName("external_id") private String externalId; + @SerializedName("cloud_sso_sign_in_url") + private String signInUrl; + @SerializedName("cloud_sso_account_id") + private String accountId; + @SerializedName("cloud_sso_access_config") + private String accessConfig; + @SerializedName("access_token") + private String accessToken; + @SerializedName("cloud_sso_access_token_expire") + private long accessTokenExpire; + @SerializedName("oauth_site_type") + private String oauthSiteType; + @SerializedName("oauth_refresh_token") + private String oauthRefreshToken; + @SerializedName("oauth_access_token") + private String oauthAccessToken; + @SerializedName("oauth_access_token_expire") + private long oauthAccessTokenExpire; + @SerializedName("sts_expiration") + private long stsExpire; public String getName() { return name; @@ -312,5 +443,73 @@ public String getPolicy() { public String getExternalId() { return externalId; } + + public String getSignInUrl() { + return signInUrl; + } + + public String getAccountId() { + return accountId; + } + + public String getAccessConfig() { + return accessConfig; + } + + public String getAccessToken() { + return accessToken; + } + + public long getAccessTokenExpire() { + return accessTokenExpire; + } + + public String getOauthSiteType() { + return oauthSiteType; + } + + public String getOauthRefreshToken() { + return oauthRefreshToken; + } + + public String getOauthAccessToken() { + return oauthAccessToken; + } + + public long getOauthAccessTokenExpire() { + return oauthAccessTokenExpire; + } + + public long getStsExpire() { + return stsExpire; + } + + public void setOauthRefreshToken(String oauthRefreshToken) { + this.oauthRefreshToken = oauthRefreshToken; + } + + public void setOauthAccessToken(String oauthAccessToken) { + this.oauthAccessToken = oauthAccessToken; + } + + public void setOauthAccessTokenExpire(long oauthAccessTokenExpire) { + this.oauthAccessTokenExpire = oauthAccessTokenExpire; + } + + public void setAccessKeyId(String accessKeyId) { + this.accessKeyId = accessKeyId; + } + + public void setAccessKeySecret(String accessKeySecret) { + this.accessKeySecret = accessKeySecret; + } + + public void setSecurityToken(String securityToken) { + this.securityToken = securityToken; + } + + public void setStsExpire(long stsExpire) { + this.stsExpire = stsExpire; + } } } diff --git a/src/main/java/com/aliyun/credentials/provider/CloudSSOCredentialsProvider.java b/src/main/java/com/aliyun/credentials/provider/CloudSSOCredentialsProvider.java new file mode 100644 index 0000000..003d904 --- /dev/null +++ b/src/main/java/com/aliyun/credentials/provider/CloudSSOCredentialsProvider.java @@ -0,0 +1,208 @@ +package com.aliyun.credentials.provider; + +import com.aliyun.credentials.exception.CredentialException; +import com.aliyun.credentials.http.*; +import com.aliyun.credentials.models.CredentialModel; +import com.aliyun.credentials.utils.AuthConstant; +import com.aliyun.credentials.utils.ParameterHelper; +import com.aliyun.credentials.utils.ProviderName; +import com.aliyun.credentials.utils.StringUtils; +import com.google.gson.Gson; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +public class CloudSSOCredentialsProvider extends SessionCredentialsProvider { + + private final String signInUrl; + private final String accountId; + private final String accessConfig; + private final String accessToken; + private final long accessTokenExpire; + private final int connectTimeout; + private final int readTimeout; + + private CloudSSOCredentialsProvider(BuilderImpl builder) { + super(builder); + + if (StringUtils.isEmpty(builder.accessToken) || builder.accessTokenExpire == 0 + || builder.accessTokenExpire - System.currentTimeMillis() / 1000 <= 0) { + throw new IllegalArgumentException("CloudSSO access token is empty or expired, please re-login with cli."); + } + if (StringUtils.isEmpty(builder.signInUrl) || StringUtils.isEmpty(builder.accountId) + || StringUtils.isEmpty(builder.accessConfig)) { + throw new IllegalArgumentException("CloudSSO sign in url, account id, and access config cannot be empty."); + } + + this.signInUrl = builder.signInUrl; + this.accountId = builder.accountId; + this.accessConfig = builder.accessConfig; + this.accessToken = builder.accessToken; + this.accessTokenExpire = builder.accessTokenExpire; + this.connectTimeout = builder.connectTimeout == null ? 5000 : builder.connectTimeout; + this.readTimeout = builder.readTimeout == null ? 10000 : builder.readTimeout; + } + + public static Builder builder() { + return new BuilderImpl(); + } + + @Override + public RefreshResult refreshCredentials() { + try (CompatibleUrlConnClient client = new CompatibleUrlConnClient()) { + return getNewSessionCredentials(client); + } + } + + RefreshResult getNewSessionCredentials(CompatibleUrlConnClient client) { + URL parsedUrl; + try { + parsedUrl = new URL(this.signInUrl); + } catch (MalformedURLException e) { + throw new CredentialException("Invalid CloudSSO sign in url: " + e.getMessage(), e); + } + + String requestUrl = parsedUrl.getProtocol() + "://" + parsedUrl.getHost() + "/cloud-credentials"; + String body = String.format("{\"AccountId\":\"%s\",\"AccessConfigurationId\":\"%s\"}", + this.accountId, this.accessConfig); + + HttpRequest httpRequest = new HttpRequest(requestUrl); + httpRequest.setSysMethod(MethodType.POST); + httpRequest.setSysConnectTimeout(this.connectTimeout); + httpRequest.setSysReadTimeout(this.readTimeout); + httpRequest.setHttpContent(body.getBytes(), "UTF-8", FormatType.JSON); + httpRequest.putHeaderParameter("Accept", "application/json"); + httpRequest.putHeaderParameter("Content-Type", "application/json"); + httpRequest.putHeaderParameter("Authorization", "Bearer " + this.accessToken); + + HttpResponse httpResponse; + try { + httpResponse = client.syncInvoke(httpRequest); + } catch (Exception e) { + throw new CredentialException("Failed to connect CloudSSO service: " + e.getMessage(), e); + } + + if (httpResponse.getResponseCode() != 200) { + throw new CredentialException(String.format( + "Get session token from CloudSSO failed, HttpCode: %s, result: %s.", + httpResponse.getResponseCode(), httpResponse.getHttpContentString())); + } + + Gson gson = new Gson(); + Map map = gson.fromJson(httpResponse.getHttpContentString(), Map.class); + if (null == map || !map.containsKey("CloudCredential")) { + throw new CredentialException(String.format( + "Get session token from CloudSSO failed, result: %s.", httpResponse.getHttpContentString())); + } + + Map result = (Map) map.get("CloudCredential"); + if (result == null || !result.containsKey("AccessKeyId") || !result.containsKey("AccessKeySecret") + || !result.containsKey("SecurityToken")) { + throw new CredentialException(String.format( + "Get session token from CloudSSO failed, fail to get credentials: %s.", + httpResponse.getHttpContentString())); + } + + long expiration = ParameterHelper.getUTCDate(result.get("Expiration")).getTime(); + CredentialModel credential = CredentialModel.builder() + .accessKeyId(result.get("AccessKeyId")) + .accessKeySecret(result.get("AccessKeySecret")) + .securityToken(result.get("SecurityToken")) + .type(AuthConstant.STS) + .providerName(this.getProviderName()) + .expiration(expiration) + .build(); + return RefreshResult.builder(credential) + .staleTime(getStaleTime(expiration)) + .build(); + } + + @Override + public String getProviderName() { + return ProviderName.CLOUD_SSO; + } + + @Override + public void close() { + super.close(); + } + + public interface Builder extends SessionCredentialsProvider.Builder { + Builder signInUrl(String signInUrl); + + Builder accountId(String accountId); + + Builder accessConfig(String accessConfig); + + Builder accessToken(String accessToken); + + Builder accessTokenExpire(long accessTokenExpire); + + Builder connectTimeout(Integer connectTimeout); + + Builder readTimeout(Integer readTimeout); + + @Override + CloudSSOCredentialsProvider build(); + } + + private static final class BuilderImpl + extends SessionCredentialsProvider.BuilderImpl + implements Builder { + private String signInUrl; + private String accountId; + private String accessConfig; + private String accessToken; + private long accessTokenExpire; + private Integer connectTimeout; + private Integer readTimeout; + + @Override + public Builder signInUrl(String signInUrl) { + this.signInUrl = signInUrl; + return this; + } + + @Override + public Builder accountId(String accountId) { + this.accountId = accountId; + return this; + } + + @Override + public Builder accessConfig(String accessConfig) { + this.accessConfig = accessConfig; + return this; + } + + @Override + public Builder accessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + @Override + public Builder accessTokenExpire(long accessTokenExpire) { + this.accessTokenExpire = accessTokenExpire; + return this; + } + + @Override + public Builder connectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + @Override + public Builder readTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + @Override + public CloudSSOCredentialsProvider build() { + return new CloudSSOCredentialsProvider(this); + } + } +} diff --git a/src/main/java/com/aliyun/credentials/provider/OAuthCredentialsProvider.java b/src/main/java/com/aliyun/credentials/provider/OAuthCredentialsProvider.java new file mode 100644 index 0000000..3c3083f --- /dev/null +++ b/src/main/java/com/aliyun/credentials/provider/OAuthCredentialsProvider.java @@ -0,0 +1,310 @@ +package com.aliyun.credentials.provider; + +import com.aliyun.credentials.exception.CredentialException; +import com.aliyun.credentials.http.*; +import com.aliyun.credentials.models.CredentialModel; +import com.aliyun.credentials.utils.AuthConstant; +import com.aliyun.credentials.utils.ParameterHelper; +import com.aliyun.credentials.utils.ProviderName; +import com.aliyun.credentials.utils.StringUtils; +import com.google.gson.Gson; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.SimpleTimeZone; + +public class OAuthCredentialsProvider extends SessionCredentialsProvider { + + @FunctionalInterface + public interface OAuthTokenUpdateCallback { + void onTokenUpdate(String refreshToken, String accessToken, String accessKeyId, + String accessKeySecret, String securityToken, + long accessTokenExpire, long stsExpire) throws Exception; + } + + private final String clientId; + private final String signInUrl; + private volatile String refreshToken; + private volatile String accessToken; + private volatile long accessTokenExpire; + private final int connectTimeout; + private final int readTimeout; + private final OAuthTokenUpdateCallback tokenUpdateCallback; + + private OAuthCredentialsProvider(BuilderImpl builder) { + super(builder); + + if (StringUtils.isEmpty(builder.clientId)) { + throw new IllegalArgumentException("The clientId is empty."); + } + if (StringUtils.isEmpty(builder.signInUrl)) { + throw new IllegalArgumentException("The url for sign-in is empty."); + } + + this.clientId = builder.clientId; + this.signInUrl = builder.signInUrl; + this.refreshToken = builder.refreshToken; + this.accessToken = builder.accessToken; + this.accessTokenExpire = builder.accessTokenExpire; + this.connectTimeout = builder.connectTimeout == null ? 5000 : builder.connectTimeout; + this.readTimeout = builder.readTimeout == null ? 10000 : builder.readTimeout; + this.tokenUpdateCallback = builder.tokenUpdateCallback; + } + + public static Builder builder() { + return new BuilderImpl(); + } + + @Override + public RefreshResult refreshCredentials() { + try (CompatibleUrlConnClient client = new CompatibleUrlConnClient()) { + return getNewSessionCredentials(client); + } + } + + RefreshResult getNewSessionCredentials(CompatibleUrlConnClient client) { + long nowSeconds = System.currentTimeMillis() / 1000; + if (!StringUtils.isEmpty(this.refreshToken) + && (StringUtils.isEmpty(this.accessToken) || this.accessTokenExpire == 0 + || this.accessTokenExpire - nowSeconds <= 1200)) { + tryRefreshOAuthToken(client); + } + + URL parsedUrl; + try { + parsedUrl = new URL(this.signInUrl); + } catch (MalformedURLException e) { + throw new CredentialException("Invalid OAuth sign in url: " + e.getMessage(), e); + } + + String requestUrl = parsedUrl.getProtocol() + "://" + parsedUrl.getHost() + "/v1/exchange"; + + HttpRequest httpRequest = new HttpRequest(requestUrl); + httpRequest.setSysMethod(MethodType.POST); + httpRequest.setSysConnectTimeout(this.connectTimeout); + httpRequest.setSysReadTimeout(this.readTimeout); + httpRequest.putHeaderParameter("Content-Type", "application/json"); + httpRequest.putHeaderParameter("Authorization", "Bearer " + this.accessToken); + + HttpResponse httpResponse; + try { + httpResponse = client.syncInvoke(httpRequest); + } catch (Exception e) { + throw new CredentialException("Failed to connect OAuth service: " + e.getMessage(), e); + } + + if (httpResponse.getResponseCode() != 200) { + throw new CredentialException(String.format( + "Get session token from OAuth failed, HttpCode: %s, result: %s.", + httpResponse.getResponseCode(), httpResponse.getHttpContentString())); + } + + Gson gson = new Gson(); + Map map = gson.fromJson(httpResponse.getHttpContentString(), Map.class); + if (null == map) { + throw new CredentialException(String.format( + "Get session token from OAuth failed, result: %s.", httpResponse.getHttpContentString())); + } + + String accessKeyId = (String) map.get("accessKeyId"); + String accessKeySecret = (String) map.get("accessKeySecret"); + String securityToken = (String) map.get("securityToken"); + String expiration = (String) map.get("expiration"); + + if (StringUtils.isEmpty(accessKeyId) || StringUtils.isEmpty(accessKeySecret) + || StringUtils.isEmpty(securityToken)) { + throw new CredentialException(String.format( + "Refresh session token from OAuth failed, fail to get credentials: %s.", + httpResponse.getHttpContentString())); + } + + long expirationMs = ParameterHelper.getUTCDate(expiration).getTime(); + + if (this.tokenUpdateCallback != null) { + try { + this.tokenUpdateCallback.onTokenUpdate(this.refreshToken, this.accessToken, + accessKeyId, accessKeySecret, securityToken, + this.accessTokenExpire, expirationMs / 1000); + } catch (Exception e) { + // Warning only, do not break credential retrieval + } + } + + CredentialModel credential = CredentialModel.builder() + .accessKeyId(accessKeyId) + .accessKeySecret(accessKeySecret) + .securityToken(securityToken) + .type(AuthConstant.STS) + .providerName(this.getProviderName()) + .expiration(expirationMs) + .build(); + return RefreshResult.builder(credential) + .staleTime(getStaleTime(expirationMs)) + .build(); + } + + private void tryRefreshOAuthToken(CompatibleUrlConnClient client) { + URL parsedUrl; + try { + parsedUrl = new URL(this.signInUrl); + } catch (MalformedURLException e) { + throw new CredentialException("Invalid OAuth sign in url: " + e.getMessage(), e); + } + + String requestUrl = parsedUrl.getProtocol() + "://" + parsedUrl.getHost() + "/v1/token"; + + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + df.setTimeZone(new SimpleTimeZone(0, "UTC")); + String timestamp = df.format(new Date()); + + String body; + try { + body = "grant_type=refresh_token" + + "&refresh_token=" + URLEncoder.encode(this.refreshToken, "UTF-8") + + "&client_id=" + URLEncoder.encode(this.clientId, "UTF-8") + + "&Timestamp=" + URLEncoder.encode(timestamp, "UTF-8"); + } catch (Exception e) { + throw new CredentialException("Failed to encode token refresh request: " + e.getMessage(), e); + } + + HttpRequest httpRequest = new HttpRequest(requestUrl); + httpRequest.setSysMethod(MethodType.POST); + httpRequest.setSysConnectTimeout(this.connectTimeout); + httpRequest.setSysReadTimeout(this.readTimeout); + httpRequest.setHttpContent(body.getBytes(), "UTF-8", FormatType.FORM); + httpRequest.putHeaderParameter("Content-Type", "application/x-www-form-urlencoded"); + + HttpResponse httpResponse; + try { + httpResponse = client.syncInvoke(httpRequest); + } catch (Exception e) { + throw new CredentialException("Failed to refresh OAuth token: " + e.getMessage(), e); + } + + if (httpResponse.getResponseCode() != 200) { + throw new CredentialException(String.format( + "Failed to refresh OAuth token, status code: %d, result: %s.", + httpResponse.getResponseCode(), httpResponse.getHttpContentString())); + } + + Gson gson = new Gson(); + Map tokenResp = gson.fromJson(httpResponse.getHttpContentString(), Map.class); + if (tokenResp == null) { + throw new CredentialException("Failed to refresh OAuth token: empty response."); + } + + String newAccessToken = (String) tokenResp.get("access_token"); + String newRefreshToken = (String) tokenResp.get("refresh_token"); + Double expiresIn = (Double) tokenResp.get("expires_in"); + + if (StringUtils.isEmpty(newAccessToken) || StringUtils.isEmpty(newRefreshToken)) { + throw new CredentialException(String.format( + "Failed to refresh OAuth token: %s.", httpResponse.getHttpContentString())); + } + + this.accessToken = newAccessToken; + this.refreshToken = newRefreshToken; + this.accessTokenExpire = System.currentTimeMillis() / 1000 + (expiresIn != null ? expiresIn.longValue() : 3600); + } + + @Override + public String getProviderName() { + return ProviderName.OAUTH; + } + + @Override + public void close() { + super.close(); + } + + public interface Builder extends SessionCredentialsProvider.Builder { + Builder clientId(String clientId); + + Builder signInUrl(String signInUrl); + + Builder refreshToken(String refreshToken); + + Builder accessToken(String accessToken); + + Builder accessTokenExpire(long accessTokenExpire); + + Builder connectTimeout(Integer connectTimeout); + + Builder readTimeout(Integer readTimeout); + + Builder tokenUpdateCallback(OAuthTokenUpdateCallback callback); + + @Override + OAuthCredentialsProvider build(); + } + + private static final class BuilderImpl + extends SessionCredentialsProvider.BuilderImpl + implements Builder { + private String clientId; + private String signInUrl; + private String refreshToken; + private String accessToken; + private long accessTokenExpire; + private Integer connectTimeout; + private Integer readTimeout; + private OAuthTokenUpdateCallback tokenUpdateCallback; + + @Override + public Builder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + @Override + public Builder signInUrl(String signInUrl) { + this.signInUrl = signInUrl; + return this; + } + + @Override + public Builder refreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + @Override + public Builder accessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + @Override + public Builder accessTokenExpire(long accessTokenExpire) { + this.accessTokenExpire = accessTokenExpire; + return this; + } + + @Override + public Builder connectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + @Override + public Builder readTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + @Override + public Builder tokenUpdateCallback(OAuthTokenUpdateCallback callback) { + this.tokenUpdateCallback = callback; + return this; + } + + @Override + public OAuthCredentialsProvider build() { + return new OAuthCredentialsProvider(this); + } + } +} diff --git a/src/main/java/com/aliyun/credentials/utils/ProviderName.java b/src/main/java/com/aliyun/credentials/utils/ProviderName.java index dd84327..5cd758c 100644 --- a/src/main/java/com/aliyun/credentials/utils/ProviderName.java +++ b/src/main/java/com/aliyun/credentials/utils/ProviderName.java @@ -9,6 +9,9 @@ public final class ProviderName { public static final String OIDC_ROLE_ARN = "oidc_role_arn"; public static final String CREDENTIALS_URI = "credentials_uri"; + public static final String OAUTH = "oauth"; + public static final String CLOUD_SSO = "cloud_sso"; + public static final String ENV = "env"; public static final String SYSTEM = "system"; public static final String PROFILE = "profile"; diff --git a/src/test/java/com/aliyun/credentials/provider/CLIProfileCredentialsProviderTest.java b/src/test/java/com/aliyun/credentials/provider/CLIProfileCredentialsProviderTest.java index 6cdc02c..1bcf718 100644 --- a/src/test/java/com/aliyun/credentials/provider/CLIProfileCredentialsProviderTest.java +++ b/src/test/java/com/aliyun/credentials/provider/CLIProfileCredentialsProviderTest.java @@ -149,6 +149,57 @@ public void reloadCredentialsProviderTest() { } } + @Test + public void testCloudSSOMode() { + CLIProfileCredentialsProvider provider = CLIProfileCredentialsProvider.builder().build(); + String configPath = CLIProfileCredentialsProviderTest.class.getClassLoader(). + getResource(".aliyun/config.json").getPath(); + CLIProfileCredentialsProvider.Config config = provider.parseProfile(configPath); + + AlibabaCloudCredentialsProvider credentialsProvider = provider.reloadCredentialsProvider(config, "CloudSSO"); + Assert.assertTrue(credentialsProvider instanceof CloudSSOCredentialsProvider); + Assert.assertEquals("cloud_sso", credentialsProvider.getProviderName()); + } + + @Test + public void testOAuthModeCN() { + CLIProfileCredentialsProvider provider = CLIProfileCredentialsProvider.builder().build(); + String configPath = CLIProfileCredentialsProviderTest.class.getClassLoader(). + getResource(".aliyun/config.json").getPath(); + CLIProfileCredentialsProvider.Config config = provider.parseProfile(configPath); + + AlibabaCloudCredentialsProvider credentialsProvider = provider.reloadCredentialsProvider(config, "OAuthCN"); + Assert.assertTrue(credentialsProvider instanceof OAuthCredentialsProvider); + Assert.assertEquals("oauth", credentialsProvider.getProviderName()); + } + + @Test + public void testOAuthModeINTL() { + CLIProfileCredentialsProvider provider = CLIProfileCredentialsProvider.builder().build(); + String configPath = CLIProfileCredentialsProviderTest.class.getClassLoader(). + getResource(".aliyun/config.json").getPath(); + CLIProfileCredentialsProvider.Config config = provider.parseProfile(configPath); + + AlibabaCloudCredentialsProvider credentialsProvider = provider.reloadCredentialsProvider(config, "OAuthINTL"); + Assert.assertTrue(credentialsProvider instanceof OAuthCredentialsProvider); + Assert.assertEquals("oauth", credentialsProvider.getProviderName()); + } + + @Test + public void testOAuthModeInvalidSiteType() { + CLIProfileCredentialsProvider provider = CLIProfileCredentialsProvider.builder().build(); + String configPath = CLIProfileCredentialsProviderTest.class.getClassLoader(). + getResource(".aliyun/config.json").getPath(); + CLIProfileCredentialsProvider.Config config = provider.parseProfile(configPath); + + try { + provider.reloadCredentialsProvider(config, "OAuthInvalid"); + Assert.fail(); + } catch (CredentialException e) { + Assert.assertEquals("Invalid OAuth site type, support CN or INTL.", e.getMessage()); + } + } + @Test public void getCredentialsTest() { String homePath = System.getProperty("user.home"); diff --git a/src/test/java/com/aliyun/credentials/provider/CloudSSOCredentialsProviderTest.java b/src/test/java/com/aliyun/credentials/provider/CloudSSOCredentialsProviderTest.java new file mode 100644 index 0000000..1726390 --- /dev/null +++ b/src/test/java/com/aliyun/credentials/provider/CloudSSOCredentialsProviderTest.java @@ -0,0 +1,202 @@ +package com.aliyun.credentials.provider; + +import com.aliyun.credentials.exception.CredentialException; +import com.aliyun.credentials.http.CompatibleUrlConnClient; +import com.aliyun.credentials.http.FormatType; +import com.aliyun.credentials.http.HttpRequest; +import com.aliyun.credentials.http.HttpResponse; +import com.aliyun.credentials.models.CredentialModel; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.ArgumentMatchers; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CloudSSOCredentialsProviderTest { + + @Test + public void testBuilderValidation() { + try { + CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.aliyuncs.com") + .accountId("123456") + .accessConfig("ac-config") + .accessToken("") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals("CloudSSO access token is empty or expired, please re-login with cli.", e.getMessage()); + } + + try { + CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.aliyuncs.com") + .accountId("123456") + .accessConfig("ac-config") + .accessToken("token") + .accessTokenExpire(0) + .build(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals("CloudSSO access token is empty or expired, please re-login with cli.", e.getMessage()); + } + + try { + CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.aliyuncs.com") + .accountId("123456") + .accessConfig("ac-config") + .accessToken("token") + .accessTokenExpire(1) + .build(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals("CloudSSO access token is empty or expired, please re-login with cli.", e.getMessage()); + } + + try { + CloudSSOCredentialsProvider.builder() + .accountId("123456") + .accessConfig("ac-config") + .accessToken("token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals("CloudSSO sign in url, account id, and access config cannot be empty.", e.getMessage()); + } + + try { + CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.aliyuncs.com") + .accessConfig("ac-config") + .accessToken("token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals("CloudSSO sign in url, account id, and access config cannot be empty.", e.getMessage()); + } + + try { + CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.aliyuncs.com") + .accountId("123456") + .accessToken("token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals("CloudSSO sign in url, account id, and access config cannot be empty.", e.getMessage()); + } + } + + @Test + public void testGetNewSessionCredentials() { + CloudSSOCredentialsProvider provider = CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.aliyuncs.com") + .accountId("123456") + .accessConfig("ac-config") + .accessToken("valid-token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + HttpResponse response = new HttpResponse("test?test=test"); + response.setResponseCode(200); + response.setHttpContent(("{\"CloudCredential\":{\"AccessKeyId\":\"ak\",\"AccessKeySecret\":\"sk\"," + + "\"SecurityToken\":\"token\",\"Expiration\":\"2019-12-12T1:1:1Z\"}}").getBytes(), "UTF-8", FormatType.JSON); + when(client.syncInvoke(ArgumentMatchers.any())).thenReturn(response); + + RefreshResult result = provider.getNewSessionCredentials(client); + Assert.assertNotNull(result); + CredentialModel credential = result.value(); + Assert.assertEquals("ak", credential.getAccessKeyId()); + Assert.assertEquals("sk", credential.getAccessKeySecret()); + Assert.assertEquals("token", credential.getSecurityToken()); + provider.close(); + } + + @Test + public void testGetNewSessionCredentialsError() { + CloudSSOCredentialsProvider provider = CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.aliyuncs.com") + .accountId("123456") + .accessConfig("ac-config") + .accessToken("valid-token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + HttpResponse response = new HttpResponse("test?test=test"); + response.setResponseCode(400); + response.setHttpContent("Bad Request".getBytes(), "UTF-8", FormatType.JSON); + when(client.syncInvoke(ArgumentMatchers.any())).thenReturn(response); + + try { + provider.getNewSessionCredentials(client); + Assert.fail(); + } catch (CredentialException e) { + Assert.assertTrue(e.getMessage().contains("Get session token from CloudSSO failed, HttpCode: 400")); + } + provider.close(); + } + + @Test + public void testGetNewSessionCredentialsInvalidJson() { + CloudSSOCredentialsProvider provider = CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.aliyuncs.com") + .accountId("123456") + .accessConfig("ac-config") + .accessToken("valid-token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + HttpResponse response = new HttpResponse("test?test=test"); + response.setResponseCode(200); + response.setHttpContent("{\"invalid\":\"response\"}".getBytes(), "UTF-8", FormatType.JSON); + when(client.syncInvoke(ArgumentMatchers.any())).thenReturn(response); + + try { + provider.getNewSessionCredentials(client); + Assert.fail(); + } catch (CredentialException e) { + Assert.assertTrue(e.getMessage().contains("Get session token from CloudSSO failed")); + } + provider.close(); + } + + @Test + public void testGetProviderName() { + CloudSSOCredentialsProvider provider = CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.aliyuncs.com") + .accountId("123456") + .accessConfig("ac-config") + .accessToken("valid-token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + Assert.assertEquals("cloud_sso", provider.getProviderName()); + provider.close(); + } + + @Test + public void testBuilder() { + long expire = System.currentTimeMillis() / 1000 + 7200; + CloudSSOCredentialsProvider provider = CloudSSOCredentialsProvider.builder() + .signInUrl("https://signin.example.com") + .accountId("account-123") + .accessConfig("config-456") + .accessToken("my-token") + .accessTokenExpire(expire) + .connectTimeout(3000) + .readTimeout(6000) + .build(); + + Assert.assertNotNull(provider); + Assert.assertEquals("cloud_sso", provider.getProviderName()); + provider.close(); + } +} diff --git a/src/test/java/com/aliyun/credentials/provider/OAuthCredentialsProviderTest.java b/src/test/java/com/aliyun/credentials/provider/OAuthCredentialsProviderTest.java new file mode 100644 index 0000000..5965620 --- /dev/null +++ b/src/test/java/com/aliyun/credentials/provider/OAuthCredentialsProviderTest.java @@ -0,0 +1,256 @@ +package com.aliyun.credentials.provider; + +import com.aliyun.credentials.exception.CredentialException; +import com.aliyun.credentials.http.CompatibleUrlConnClient; +import com.aliyun.credentials.http.FormatType; +import com.aliyun.credentials.http.HttpRequest; +import com.aliyun.credentials.http.HttpResponse; +import com.aliyun.credentials.models.CredentialModel; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OAuthCredentialsProviderTest { + + @Test + public void testBuilderValidation() { + try { + OAuthCredentialsProvider.builder() + .clientId("") + .signInUrl("https://oauth.aliyun.com") + .build(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals("The clientId is empty.", e.getMessage()); + } + + try { + OAuthCredentialsProvider.builder() + .clientId("client-id") + .signInUrl("") + .build(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals("The url for sign-in is empty.", e.getMessage()); + } + + try { + OAuthCredentialsProvider.builder() + .clientId("client-id") + .build(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals("The url for sign-in is empty.", e.getMessage()); + } + } + + @Test + public void testGetNewSessionCredentials() { + OAuthCredentialsProvider provider = OAuthCredentialsProvider.builder() + .clientId("test-client") + .signInUrl("https://oauth.aliyun.com") + .accessToken("valid-access-token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + HttpResponse response = new HttpResponse("test?test=test"); + response.setResponseCode(200); + response.setHttpContent(("{\"accessKeyId\":\"ak\",\"accessKeySecret\":\"sk\"," + + "\"securityToken\":\"token\",\"expiration\":\"2019-12-12T1:1:1Z\"}").getBytes(), "UTF-8", FormatType.JSON); + when(client.syncInvoke(ArgumentMatchers.any())).thenReturn(response); + + RefreshResult result = provider.getNewSessionCredentials(client); + Assert.assertNotNull(result); + CredentialModel credential = result.value(); + Assert.assertEquals("ak", credential.getAccessKeyId()); + Assert.assertEquals("sk", credential.getAccessKeySecret()); + Assert.assertEquals("token", credential.getSecurityToken()); + provider.close(); + } + + @Test + public void testGetNewSessionCredentialsError() { + OAuthCredentialsProvider provider = OAuthCredentialsProvider.builder() + .clientId("test-client") + .signInUrl("https://oauth.aliyun.com") + .accessToken("valid-access-token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + HttpResponse response = new HttpResponse("test?test=test"); + response.setResponseCode(400); + response.setHttpContent("Bad Request".getBytes(), "UTF-8", FormatType.JSON); + when(client.syncInvoke(ArgumentMatchers.any())).thenReturn(response); + + try { + provider.getNewSessionCredentials(client); + Assert.fail(); + } catch (CredentialException e) { + Assert.assertTrue(e.getMessage().contains("Get session token from OAuth failed, HttpCode: 400")); + } + provider.close(); + } + + @Test + public void testGetNewSessionCredentialsInvalidJson() { + OAuthCredentialsProvider provider = OAuthCredentialsProvider.builder() + .clientId("test-client") + .signInUrl("https://oauth.aliyun.com") + .accessToken("valid-access-token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + HttpResponse response = new HttpResponse("test?test=test"); + response.setResponseCode(200); + response.setHttpContent("not json at all".getBytes(), "UTF-8", FormatType.JSON); + when(client.syncInvoke(ArgumentMatchers.any())).thenReturn(response); + + try { + provider.getNewSessionCredentials(client); + Assert.fail(); + } catch (Exception e) { + Assert.assertNotNull(e.getMessage()); + } + provider.close(); + } + + @Test + public void testGetNewSessionCredentialsMissingFields() { + OAuthCredentialsProvider provider = OAuthCredentialsProvider.builder() + .clientId("test-client") + .signInUrl("https://oauth.aliyun.com") + .accessToken("valid-access-token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + HttpResponse response = new HttpResponse("test?test=test"); + response.setResponseCode(200); + response.setHttpContent(("{\"accessKeyId\":\"\",\"accessKeySecret\":\"sk\"," + + "\"securityToken\":\"token\",\"expiration\":\"2019-12-12T1:1:1Z\"}").getBytes(), "UTF-8", FormatType.JSON); + when(client.syncInvoke(ArgumentMatchers.any())).thenReturn(response); + + try { + provider.getNewSessionCredentials(client); + Assert.fail(); + } catch (CredentialException e) { + Assert.assertTrue(e.getMessage().contains("Refresh session token from OAuth failed, fail to get credentials")); + } + provider.close(); + } + + @Test + public void testTryRefreshOAuthToken() { + OAuthCredentialsProvider provider = OAuthCredentialsProvider.builder() + .clientId("test-client") + .signInUrl("https://oauth.aliyun.com") + .refreshToken("old-refresh-token") + .accessToken("expired-token") + .accessTokenExpire(0) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + + HttpResponse refreshResponse = new HttpResponse("test?test=test"); + refreshResponse.setResponseCode(200); + refreshResponse.setHttpContent(("{\"access_token\":\"new\",\"refresh_token\":\"new_refresh\"," + + "\"expires_in\":3600}").getBytes(), "UTF-8", FormatType.JSON); + + HttpResponse exchangeResponse = new HttpResponse("test?test=test"); + exchangeResponse.setResponseCode(200); + exchangeResponse.setHttpContent(("{\"accessKeyId\":\"ak\",\"accessKeySecret\":\"sk\"," + + "\"securityToken\":\"token\",\"expiration\":\"2019-12-12T1:1:1Z\"}").getBytes(), "UTF-8", FormatType.JSON); + + when(client.syncInvoke(ArgumentMatchers.any())) + .thenReturn(refreshResponse) + .thenReturn(exchangeResponse); + + RefreshResult result = provider.getNewSessionCredentials(client); + Assert.assertNotNull(result); + CredentialModel credential = result.value(); + Assert.assertEquals("ak", credential.getAccessKeyId()); + Assert.assertEquals("sk", credential.getAccessKeySecret()); + Assert.assertEquals("token", credential.getSecurityToken()); + provider.close(); + } + + @Test + public void testTryRefreshOAuthTokenError() { + OAuthCredentialsProvider provider = OAuthCredentialsProvider.builder() + .clientId("test-client") + .signInUrl("https://oauth.aliyun.com") + .refreshToken("old-refresh-token") + .accessToken("expired-token") + .accessTokenExpire(0) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + HttpResponse response = new HttpResponse("test?test=test"); + response.setResponseCode(400); + response.setHttpContent("token refresh failed".getBytes(), "UTF-8", FormatType.JSON); + when(client.syncInvoke(ArgumentMatchers.any())).thenReturn(response); + + try { + provider.getNewSessionCredentials(client); + Assert.fail(); + } catch (CredentialException e) { + Assert.assertTrue(e.getMessage().contains("Failed to refresh OAuth token, status code: 400")); + } + provider.close(); + } + + @Test + public void testTokenUpdateCallback() { + final AtomicBoolean callbackInvoked = new AtomicBoolean(false); + final AtomicReference capturedAk = new AtomicReference<>(); + + OAuthCredentialsProvider.OAuthTokenUpdateCallback callback = + (refreshToken, accessToken, accessKeyId, accessKeySecret, securityToken, accessTokenExpire, stsExpire) -> { + callbackInvoked.set(true); + capturedAk.set(accessKeyId); + }; + + OAuthCredentialsProvider provider = OAuthCredentialsProvider.builder() + .clientId("test-client") + .signInUrl("https://oauth.aliyun.com") + .accessToken("valid-access-token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .tokenUpdateCallback(callback) + .build(); + + CompatibleUrlConnClient client = mock(CompatibleUrlConnClient.class); + HttpResponse response = new HttpResponse("test?test=test"); + response.setResponseCode(200); + response.setHttpContent(("{\"accessKeyId\":\"ak\",\"accessKeySecret\":\"sk\"," + + "\"securityToken\":\"token\",\"expiration\":\"2019-12-12T1:1:1Z\"}").getBytes(), "UTF-8", FormatType.JSON); + when(client.syncInvoke(ArgumentMatchers.any())).thenReturn(response); + + provider.getNewSessionCredentials(client); + Assert.assertTrue(callbackInvoked.get()); + Assert.assertEquals("ak", capturedAk.get()); + provider.close(); + } + + @Test + public void testGetProviderName() { + OAuthCredentialsProvider provider = OAuthCredentialsProvider.builder() + .clientId("test-client") + .signInUrl("https://oauth.aliyun.com") + .accessToken("token") + .accessTokenExpire(System.currentTimeMillis() / 1000 + 3600) + .build(); + Assert.assertEquals("oauth", provider.getProviderName()); + provider.close(); + } +} diff --git a/src/test/resources/.aliyun/config.json b/src/test/resources/.aliyun/config.json index 6beef91..1bd04a5 100644 --- a/src/test/resources/.aliyun/config.json +++ b/src/test/resources/.aliyun/config.json @@ -52,6 +52,39 @@ { "name": "Unsupported", "mode": "Unsupported" + }, + { + "name": "CloudSSO", + "mode": "CloudSSO", + "cloud_sso_sign_in_url": "https://signin.aliyuncs.com", + "cloud_sso_account_id": "account-123", + "cloud_sso_access_config": "ac-config-456", + "access_token": "sso-token", + "cloud_sso_access_token_expire": 9999999999 + }, + { + "name": "OAuthCN", + "mode": "OAuth", + "oauth_site_type": "CN", + "oauth_refresh_token": "cn-refresh-token", + "oauth_access_token": "cn-access-token", + "oauth_access_token_expire": 9999999999 + }, + { + "name": "OAuthINTL", + "mode": "OAuth", + "oauth_site_type": "INTL", + "oauth_refresh_token": "intl-refresh-token", + "oauth_access_token": "intl-access-token", + "oauth_access_token_expire": 9999999999 + }, + { + "name": "OAuthInvalid", + "mode": "OAuth", + "oauth_site_type": "INVALID", + "oauth_refresh_token": "refresh-token", + "oauth_access_token": "access-token", + "oauth_access_token_expire": 9999999999 } ] } \ No newline at end of file