Skip to content

Commit f899ddc

Browse files
committed
Add Login Credentials to Credential Provider Chain
1 parent c6d8c6c commit f899ddc

File tree

5 files changed

+275
-0
lines changed

5 files changed

+275
-0
lines changed

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/ProfileCredentialsUtils.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
3636
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
3737
import software.amazon.awssdk.auth.credentials.ProcessCredentialsProvider;
38+
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
3839
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProviderFactory;
3940
import software.amazon.awssdk.auth.credentials.ProfileProviderCredentialsContext;
4041
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
@@ -57,6 +58,8 @@ public final class ProfileCredentialsUtils {
5758
"software.amazon.awssdk.services.sts.internal.StsProfileCredentialsProviderFactory";
5859
private static final String SSO_PROFILE_CREDENTIALS_PROVIDER_FACTORY =
5960
"software.amazon.awssdk.services.sso.auth.SsoProfileCredentialsProviderFactory";
61+
private static final String LOGIN_PROFILE_CREDENTIALS_PROVIDER_FACTORY =
62+
"software.amazon.awssdk.services.signin.auth.LoginProfileCredentialsProviderFactory";
6063

6164
/**
6265
* The profile file containing {@code profile}.
@@ -144,6 +147,10 @@ private Optional<CredentialsWithFeatureId> credentialsProviderWithFeatureID(Set<
144147
}
145148
}
146149

150+
if (properties.containsKey(ProfileProperty.LOGIN_SESSION)) {
151+
return Optional.of(loginProfileCredentialsProvider());
152+
}
153+
147154
if (properties.containsKey(ProfileProperty.CREDENTIAL_PROCESS)) {
148155
return Optional.of(credentialProcessCredentialsProvider());
149156
}
@@ -243,6 +250,20 @@ private boolean isLegacySsoConfiguration() {
243250
return !properties.containsKey(ProfileSection.SSO_SESSION.getPropertyKeyName());
244251
}
245252

253+
/**
254+
* Create the SSO credentials provider based on the related profile properties.
255+
*/
256+
private CredentialsWithFeatureId loginProfileCredentialsProvider() {
257+
AwsCredentialsProvider provider = loginCredentialsProviderFactory().create(
258+
ProfileProviderCredentialsContext.builder()
259+
.profile(profile)
260+
.profileFile(profileFile)
261+
.sourceChain(BusinessMetricFeatureId.CREDENTIALS_PROFILE_LOGIN.value())
262+
.build());
263+
264+
return new CredentialsWithFeatureId(provider, BusinessMetricFeatureId.CREDENTIALS_PROFILE_LOGIN.value());
265+
}
266+
246267
private CredentialsWithFeatureId roleAndWebIdentityTokenProfileCredentialsProvider() {
247268
requireProperties(ProfileProperty.ROLE_ARN, ProfileProperty.WEB_IDENTITY_TOKEN_FILE);
248269

@@ -418,4 +439,17 @@ private ProfileCredentialsProviderFactory ssoCredentialsProviderFactory() {
418439
throw new IllegalStateException("Failed to create the '" + name + "' profile credentials provider.", e);
419440
}
420441
}
442+
443+
private ProfileCredentialsProviderFactory loginCredentialsProviderFactory() {
444+
try {
445+
Class<?> loginProfileCredentialsProviderFactory =
446+
ClassLoaderHelper.loadClass(LOGIN_PROFILE_CREDENTIALS_PROVIDER_FACTORY, getClass());
447+
return (ProfileCredentialsProviderFactory) loginProfileCredentialsProviderFactory.getConstructor().newInstance();
448+
} catch (ClassNotFoundException e) {
449+
throw new IllegalStateException("To use login_session property in the '" + name + "' profile, the 'signin' service "
450+
+ "module must be on the class path.", e);
451+
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
452+
throw new IllegalStateException("Failed to create the '" + name + "' profile credentials provider.", e);
453+
}
454+
}
421455
}

core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ public final class ProfileProperty {
201201
*/
202202
public static final String SIGV4A_SIGNING_REGION_SET = "sigv4a_signing_region_set";
203203

204+
/**
205+
* Property name for login session used with AWS Login/Sign-In Credentials.
206+
*/
207+
public static final String LOGIN_SESSION = "login_session";
208+
204209
private ProfileProperty() {
205210
}
206211
}

services/signin/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,11 @@
5656
<artifactId>http-auth-aws</artifactId>
5757
<version>${awsjavasdk.version}</version>
5858
</dependency>
59+
<dependency>
60+
<groupId>software.amazon.awssdk</groupId>
61+
<artifactId>profiles</artifactId>
62+
<version>${awsjavasdk.version}</version>
63+
<scope>compile</scope>
64+
</dependency>
5965
</dependencies>
6066
</project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.signin.auth;
17+
18+
import software.amazon.awssdk.annotations.SdkProtectedApi;
19+
import software.amazon.awssdk.auth.credentials.AwsCredentials;
20+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
21+
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProviderFactory;
22+
import software.amazon.awssdk.auth.credentials.ProfileProviderCredentialsContext;
23+
import software.amazon.awssdk.profiles.Profile;
24+
import software.amazon.awssdk.profiles.ProfileProperty;
25+
import software.amazon.awssdk.services.signin.SigninClient;
26+
import software.amazon.awssdk.utils.IoUtils;
27+
import software.amazon.awssdk.utils.SdkAutoCloseable;
28+
29+
/**
30+
* An implementation of {@link ProfileCredentialsProviderFactory} that allows users to get login credentials using the
31+
* login_session specified in a {@link Profile}.
32+
*/
33+
@SdkProtectedApi
34+
public class LoginProfileCredentialsProviderFactory implements ProfileCredentialsProviderFactory {
35+
36+
/**
37+
* Default method to create the {@link LoginProfileCredentialsProviderFactory} object created
38+
* with the login_session from {@link Profile} in the {@link ProfileProviderCredentialsContext}.
39+
*/
40+
@Override
41+
public AwsCredentialsProvider create(ProfileProviderCredentialsContext profileProviderCredentialsContext) {
42+
return new LoginProfileCredentialsProvider(profileProviderCredentialsContext);
43+
}
44+
45+
private static class LoginProfileCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable {
46+
private final LoginCredentialsProvider credentialsProvider;
47+
private final SigninClient signinClient;
48+
49+
private LoginProfileCredentialsProvider(ProfileProviderCredentialsContext credentialsContext) {
50+
Profile profile = credentialsContext.profile();
51+
String loginSession = profile.property(ProfileProperty.LOGIN_SESSION)
52+
.orElseThrow(() -> new IllegalArgumentException("login_session property is required"));
53+
54+
this.signinClient = SigninClient.create();
55+
this.credentialsProvider = LoginCredentialsProvider
56+
.builder()
57+
.loginSession(loginSession)
58+
.signinClient(signinClient)
59+
.sourceChain(credentialsContext.sourceChain())
60+
.build();
61+
62+
}
63+
64+
@Override
65+
public AwsCredentials resolveCredentials() {
66+
return this.credentialsProvider.resolveCredentials();
67+
}
68+
69+
@Override
70+
public void close() {
71+
IoUtils.closeQuietly(credentialsProvider, null);
72+
IoUtils.closeQuietly(signinClient, null);
73+
}
74+
75+
}
76+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.signin.auth;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
20+
import static org.junit.jupiter.api.Assertions.assertTrue;
21+
import static software.amazon.awssdk.services.signin.auth.internal.DpopTestUtils.VALID_TEST_PEM;
22+
23+
import java.nio.file.Path;
24+
import java.time.Instant;
25+
import java.util.Optional;
26+
import org.junit.jupiter.api.AfterEach;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.io.TempDir;
30+
import software.amazon.awssdk.auth.credentials.AwsCredentials;
31+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
32+
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
33+
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
34+
import software.amazon.awssdk.auth.credentials.ProfileProviderCredentialsContext;
35+
import software.amazon.awssdk.auth.credentials.internal.ProfileCredentialsUtils;
36+
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;
37+
import software.amazon.awssdk.profiles.ProfileFile;
38+
import software.amazon.awssdk.services.signin.internal.AccessTokenManager;
39+
import software.amazon.awssdk.services.signin.internal.LoginAccessToken;
40+
import software.amazon.awssdk.services.signin.internal.LoginCacheDirectorySystemSetting;
41+
import software.amazon.awssdk.services.signin.internal.OnDiskTokenManager;
42+
import software.amazon.awssdk.utils.StringInputStream;
43+
44+
public class LoginProfileCredentialsProviderFactoryTest {
45+
private static final String LOGIN_SESSION_ID = "loginSessionId";
46+
47+
@TempDir
48+
Path tempDir;
49+
50+
private AccessTokenManager tokenManager;
51+
52+
@BeforeEach
53+
public void setup() {
54+
System.setProperty(new LoginCacheDirectorySystemSetting().property(), tempDir.toString());
55+
tokenManager = OnDiskTokenManager.create(tempDir, LOGIN_SESSION_ID);
56+
}
57+
58+
@AfterEach
59+
public void teardown() {
60+
System.clearProperty(new LoginCacheDirectorySystemSetting().property());
61+
}
62+
63+
@Test
64+
public void create_returnsLoginCredentialsFromProfile() {
65+
AwsSessionCredentials creds = buildCredentials( Instant.now().plusSeconds(600));
66+
LoginAccessToken token = buildAccessToken(creds);
67+
tokenManager.storeToken(token);
68+
69+
ProfileFile profileFile = configFile("[profile foo]\n" +
70+
"login_session=" + LOGIN_SESSION_ID + "\n");
71+
72+
AwsCredentialsProvider provider = new LoginProfileCredentialsProviderFactory().create(
73+
ProfileProviderCredentialsContext
74+
.builder()
75+
.profile(profileFile.profile("foo").get())
76+
.profileFile(profileFile)
77+
.build()
78+
);
79+
80+
// validate the creds are from cached token file stored above
81+
AwsCredentials credentials = provider.resolveCredentials();
82+
assertInstanceOf(AwsSessionCredentials.class, credentials);
83+
AwsSessionCredentials resolvedCredentials = (AwsSessionCredentials) credentials;
84+
assertEquals(creds.accessKeyId(), resolvedCredentials.accessKeyId());
85+
assertEquals(creds.secretAccessKey(), resolvedCredentials.secretAccessKey());
86+
assertEquals(creds.sessionToken(), resolvedCredentials.sessionToken());
87+
assertEquals(creds.accountId(), resolvedCredentials.accountId());
88+
assertEquals(BusinessMetricFeatureId.CREDENTIALS_LOGIN.value(), resolvedCredentials.providerName().get());
89+
}
90+
91+
/**
92+
* This test validates that the {@link ProfileCredentialsUtils} used in the {@link ProfileCredentialsProvider}
93+
* correctly loads and configures login session credentials using the factory.
94+
* This test exists in this module because it depends on having the `signin` module available.
95+
*/
96+
@Test
97+
public void profileCredentialsUtils_returnsLoginCredentialsFromProfile() {
98+
AwsSessionCredentials creds = buildCredentials( Instant.now().plusSeconds(600));
99+
LoginAccessToken token = buildAccessToken(creds);
100+
tokenManager.storeToken(token);
101+
102+
ProfileFile profileFile = configFile("[profile foo]\n" +
103+
"login_session=" + LOGIN_SESSION_ID + "\n");
104+
105+
ProfileCredentialsUtils profileCredentialsUtils = new ProfileCredentialsUtils(
106+
profileFile, profileFile.profile("foo").get(), profileFile::profile
107+
);
108+
109+
Optional<AwsCredentialsProvider> resolvedProvider = profileCredentialsUtils.credentialsProvider();
110+
assertTrue(resolvedProvider.isPresent());
111+
112+
// validate the creds are from cached token file stored above
113+
AwsCredentials credentials = resolvedProvider.get().resolveCredentials();
114+
assertInstanceOf(AwsSessionCredentials.class, credentials);
115+
AwsSessionCredentials resolvedCredentials = (AwsSessionCredentials) credentials;
116+
assertEquals(creds.accessKeyId(), resolvedCredentials.accessKeyId());
117+
assertEquals(creds.secretAccessKey(), resolvedCredentials.secretAccessKey());
118+
assertEquals(creds.sessionToken(), resolvedCredentials.sessionToken());
119+
assertEquals(creds.accountId(), resolvedCredentials.accountId());
120+
String expectedCredSource = BusinessMetricFeatureId.CREDENTIALS_PROFILE_LOGIN.value() + ","
121+
+ BusinessMetricFeatureId.CREDENTIALS_LOGIN.value();
122+
assertEquals(expectedCredSource, resolvedCredentials.providerName().get());
123+
}
124+
125+
private static ProfileFile configFile(String configFile) {
126+
return ProfileFile.builder()
127+
.content(new StringInputStream(configFile))
128+
.type(ProfileFile.Type.CONFIGURATION)
129+
.build();
130+
}
131+
132+
133+
134+
private AwsSessionCredentials buildCredentials(Instant expirationTime) {
135+
return AwsSessionCredentials.builder()
136+
.accessKeyId("akid")
137+
.secretAccessKey("skid")
138+
.sessionToken("sessionToken")
139+
.accountId("123456789012")
140+
.expirationTime(expirationTime)
141+
.build();
142+
}
143+
144+
private LoginAccessToken buildAccessToken(AwsSessionCredentials credentials) {
145+
return LoginAccessToken.builder()
146+
.accessToken(credentials)
147+
.clientId("client-123")
148+
.dpopKey(VALID_TEST_PEM)
149+
.refreshToken("refresh-token")
150+
.tokenType("aws_sigv4")
151+
.identityToken("id-token")
152+
.build();
153+
}
154+
}

0 commit comments

Comments
 (0)