Skip to content

Commit 62167a2

Browse files
authored
Merge pull request #4446 from aws/alexwoo/master/loginCredentialProvider_refresh_dpop
[LoginCredentialsProvider] Implement DPoP signing of request
2 parents d50d94c + c6d8c6c commit 62167a2

File tree

10 files changed

+655
-160
lines changed

10 files changed

+655
-160
lines changed

services/signin/src/main/java/software/amazon/awssdk/services/signin/auth/LoginCredentialsProvider.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
import software.amazon.awssdk.auth.credentials.AwsCredentials;
3030
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
3131
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
32+
import software.amazon.awssdk.core.SdkPlugin;
3233
import software.amazon.awssdk.core.exception.SdkClientException;
3334
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;
3435
import software.amazon.awssdk.services.signin.SigninClient;
3536
import software.amazon.awssdk.services.signin.internal.AccessTokenManager;
37+
import software.amazon.awssdk.services.signin.internal.DpopAuthPlugin;
3638
import software.amazon.awssdk.services.signin.internal.LoginAccessToken;
3739
import software.amazon.awssdk.services.signin.internal.LoginCacheDirectorySystemSetting;
3840
import software.amazon.awssdk.services.signin.internal.OnDiskTokenManager;
@@ -159,14 +161,15 @@ private RefreshResult<AwsCredentials> refreshFromSigninService(LoginAccessToken
159161
log.debug(() -> "Credentials are near expiration/expired, refreshing from Signin service.");
160162

161163
try {
164+
SdkPlugin dpopAuthPlugin = DpopAuthPlugin.create(tokenFromDisc.getDpopKey());
162165
CreateOAuth2TokenRequest refreshRequest =
163166
CreateOAuth2TokenRequest
164167
.builder()
165168
.tokenInput(t -> t
166169
.clientId(tokenFromDisc.getClientId())
167170
.refreshToken(tokenFromDisc.getRefreshToken())
168171
.grantType("refresh_token"))
169-
.dpopProof("TODO") // TODO: This will be implemented in a separate PR
172+
.overrideConfiguration(c -> c.addPlugin(dpopAuthPlugin))
170173
.build();
171174

172175
CreateOAuth2TokenResponse createTokenResponse = signinClient.createOAuth2Token(refreshRequest);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.internal;
17+
18+
import java.util.Collections;
19+
import java.util.List;
20+
import java.util.concurrent.CompletableFuture;
21+
import software.amazon.awssdk.annotations.SdkInternalApi;
22+
import software.amazon.awssdk.core.SdkPlugin;
23+
import software.amazon.awssdk.core.SdkServiceClientConfiguration;
24+
import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption;
25+
import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeProvider;
26+
import software.amazon.awssdk.identity.spi.IdentityProvider;
27+
import software.amazon.awssdk.identity.spi.ResolveIdentityRequest;
28+
import software.amazon.awssdk.services.signin.SigninServiceClientConfiguration;
29+
import software.amazon.awssdk.services.signin.auth.scheme.SigninAuthSchemeParams;
30+
import software.amazon.awssdk.services.signin.auth.scheme.SigninAuthSchemeProvider;
31+
import software.amazon.awssdk.utils.Validate;
32+
33+
/**
34+
* An SDK plugin that will use DPoP auth for requests by adding the {@link DpopAuthScheme} and overriding the
35+
* {@link AuthSchemeProvider} with a custom provider that will always return Dpop.
36+
* The auth scheme uses the {@link DpopSigner} to add the required DPoP header to the request.
37+
*/
38+
@SdkInternalApi
39+
public class DpopAuthPlugin implements SdkPlugin {
40+
private final String dpopKeyPem;
41+
42+
private DpopAuthPlugin(String dpopKeyPem) {
43+
this.dpopKeyPem = Validate.paramNotNull(dpopKeyPem, "dpopKeyPem");
44+
}
45+
46+
/**
47+
* Create an instance of the DpopAuthPlugin using the dpopKey from the {@link LoginAccessToken}
48+
* @param dpopKeyPem - PEM file contents containing the base64-encoding of the ECPrivateKey format defined by
49+
* RFC5915: Elliptic Curve Private Key Structure. It MUST include the public key coordinates.
50+
* @return dpopAuthPlugin
51+
*/
52+
public static DpopAuthPlugin create(String dpopKeyPem) {
53+
return new DpopAuthPlugin(dpopKeyPem);
54+
}
55+
56+
@Override
57+
public void configureClient(SdkServiceClientConfiguration.Builder config) {
58+
SigninServiceClientConfiguration.Builder scb =
59+
Validate.isInstanceOf(SigninServiceClientConfiguration.Builder.class, config,
60+
"DpopAuthPlugin must be applied to a SigninServiceClient");
61+
scb.authSchemeProvider(new DpopAuthSchemeProvider());
62+
// we must use a static DpopIdentity here rather than one that dynamically loads from the disk cache
63+
// the refresh request takes the clientId/refreshToken sourced from the access token on disk as input
64+
// so we must sign the request with the dpopKey loaded from the same load. IE: do not read the
65+
// access token file twice!
66+
scb.putAuthScheme(DpopAuthScheme.create(StaticDpopIdentityProvider.create(dpopKeyPem)));
67+
}
68+
69+
private static class DpopAuthSchemeProvider implements SigninAuthSchemeProvider {
70+
71+
@Override
72+
public List<AuthSchemeOption> resolveAuthScheme(SigninAuthSchemeParams authSchemeParams) {
73+
return Collections.singletonList(AuthSchemeOption.builder().schemeId(DpopAuthScheme.SCHEME_NAME).build());
74+
}
75+
}
76+
77+
/**
78+
* A identity provider that provides a static {@link DpopIdentity}
79+
*/
80+
private static class StaticDpopIdentityProvider implements IdentityProvider<DpopIdentity> {
81+
private final DpopIdentity identity;
82+
83+
private StaticDpopIdentityProvider(DpopIdentity identity) {
84+
this.identity = Validate.paramNotNull(identity, "identity");
85+
}
86+
87+
public static StaticDpopIdentityProvider create(String dpopKeyPem) {
88+
return new StaticDpopIdentityProvider(DpopIdentity.create(dpopKeyPem));
89+
}
90+
91+
@Override
92+
public Class<DpopIdentity> identityType() {
93+
return DpopIdentity.class;
94+
}
95+
96+
@Override
97+
public CompletableFuture<? extends DpopIdentity> resolveIdentity(ResolveIdentityRequest request) {
98+
return CompletableFuture.completedFuture(identity);
99+
}
100+
}
101+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.internal;
17+
18+
import software.amazon.awssdk.annotations.SdkInternalApi;
19+
import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme;
20+
import software.amazon.awssdk.http.auth.spi.signer.HttpSigner;
21+
import software.amazon.awssdk.identity.spi.IdentityProvider;
22+
import software.amazon.awssdk.identity.spi.IdentityProviders;
23+
import software.amazon.awssdk.utils.Validate;
24+
25+
/**
26+
* An AuthScheme representing authentication withOAuth 2.0 Demonstrating Proof of Possession (DPoP) header.
27+
*/
28+
@SdkInternalApi
29+
public class DpopAuthScheme implements AuthScheme<DpopIdentity> {
30+
public static final String SCHEME_NAME = "DPOP";
31+
32+
private final IdentityProvider<DpopIdentity> identityProvider;
33+
34+
private DpopAuthScheme(IdentityProvider<DpopIdentity> identityProvider) {
35+
this.identityProvider = Validate.paramNotNull(identityProvider, "identityProvider");
36+
}
37+
38+
public static DpopAuthScheme create(IdentityProvider<DpopIdentity> identityProvider) {
39+
return new DpopAuthScheme(identityProvider);
40+
}
41+
42+
@Override
43+
public String schemeId() {
44+
return SCHEME_NAME;
45+
}
46+
47+
@Override
48+
public IdentityProvider<DpopIdentity> identityProvider(IdentityProviders providers) {
49+
// we don't currently support adding an arbitrary identityProvider as a request level override
50+
// return the identity provider configured up front instead
51+
return identityProvider;
52+
}
53+
54+
@Override
55+
public HttpSigner<DpopIdentity> signer() {
56+
return new DpopSigner();
57+
}
58+
}

services/signin/src/main/java/software/amazon/awssdk/services/signin/internal/DpopHeaderGenerator.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import java.util.Base64;
2828
import software.amazon.awssdk.annotations.SdkInternalApi;
2929
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
30-
import software.amazon.awssdk.utils.Pair;
3130
import software.amazon.awssdk.utils.Validate;
3231

3332
/**
@@ -57,26 +56,25 @@ private DpopHeaderGenerator() {
5756
* <li><a href="https://datatracker.ietf.org/doc/html/rfc7517">RFC 7517 - JSON Web Key (JWK)</a></li>
5857
* </ul>
5958
*
60-
* @param pemContent - EC1 / RFC 5915 ASN.1 formated PEM contents
59+
* @param dpopIdentity - DpopIdentity containing ECPrivateKey and ECPublicKey
6160
* @param endpoint - The HTTP target URI (Section 7.1 of [RFC9110]) of the request to which the JWT is attached,
6261
* without query and fragment parts
6362
* @param httpMethod - the HTTP method of the request (eg: POST).
6463
* @param epochSeconds - creation time of the JWT in epoch seconds.
6564
* @param uuid - Unique identifier for the DPoP proof JWT - should be a UUID4 string.
6665
* @return DPoP header value
6766
*/
68-
public static String generateDPoPProofHeader(String pemContent, String endpoint, String httpMethod,
67+
public static String generateDPoPProofHeader(DpopIdentity dpopIdentity, String endpoint, String httpMethod,
6968
long epochSeconds, String uuid) {
70-
Validate.paramNotBlank(pemContent, "pemContent");
69+
Validate.paramNotNull(dpopIdentity, "dpopIdentity");
7170
Validate.paramNotBlank(endpoint, "endpoint");
7271
Validate.paramNotBlank(httpMethod, "httpMethod");
7372
Validate.paramNotBlank(uuid, "uuid");
7473

7574
try {
7675
// Load EC public and private key from PEM
77-
Pair<ECPrivateKey, ECPublicKey> keys = EcKeyLoader.loadSec1Pem(pemContent);
78-
ECPrivateKey privateKey = keys.left();
79-
ECPublicKey publicKey = keys.right();
76+
ECPrivateKey privateKey = dpopIdentity.getPrivateKey();
77+
ECPublicKey publicKey = dpopIdentity.getPublicKey();
8078

8179
// Build JSON strings (header, payload) with JsonGenerator
8280
byte[] headerJson = buildHeaderJson(publicKey);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.internal;
17+
18+
import java.security.interfaces.ECPrivateKey;
19+
import java.security.interfaces.ECPublicKey;
20+
import software.amazon.awssdk.annotations.SdkInternalApi;
21+
import software.amazon.awssdk.identity.spi.Identity;
22+
import software.amazon.awssdk.utils.Pair;
23+
24+
/**
25+
* An identity representing the public and private keys required to sign a request using DPoP.
26+
*/
27+
@SdkInternalApi
28+
public class DpopIdentity implements Identity {
29+
private final ECPublicKey publicKey;
30+
private final ECPrivateKey privateKey;
31+
32+
private DpopIdentity(ECPublicKey publicKey, ECPrivateKey privateKey) {
33+
this.publicKey = publicKey;
34+
this.privateKey = privateKey;
35+
}
36+
37+
public static DpopIdentity create(ECPublicKey publicKey, ECPrivateKey privateKey) {
38+
return new DpopIdentity(publicKey, privateKey);
39+
}
40+
41+
public static DpopIdentity create(String dpopKeyPem) {
42+
Pair<ECPrivateKey, ECPublicKey> keys = EcKeyLoader.loadSec1Pem(dpopKeyPem);
43+
return new DpopIdentity(keys.right(), keys.left());
44+
}
45+
46+
public ECPublicKey getPublicKey() {
47+
return publicKey;
48+
}
49+
50+
public ECPrivateKey getPrivateKey() {
51+
return privateKey;
52+
}
53+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.internal;
17+
18+
import java.time.Instant;
19+
import java.util.UUID;
20+
import java.util.concurrent.CompletableFuture;
21+
import software.amazon.awssdk.annotations.SdkInternalApi;
22+
import software.amazon.awssdk.http.SdkHttpRequest;
23+
import software.amazon.awssdk.http.auth.spi.signer.AsyncSignRequest;
24+
import software.amazon.awssdk.http.auth.spi.signer.AsyncSignedRequest;
25+
import software.amazon.awssdk.http.auth.spi.signer.BaseSignRequest;
26+
import software.amazon.awssdk.http.auth.spi.signer.HttpSigner;
27+
import software.amazon.awssdk.http.auth.spi.signer.SignRequest;
28+
import software.amazon.awssdk.http.auth.spi.signer.SignedRequest;
29+
import software.amazon.awssdk.utils.http.SdkHttpUtils;
30+
31+
/**
32+
* Signs request with a DPoP header using the requests endpoint and http method and the
33+
* key from the resolved Identity.
34+
*/
35+
@SdkInternalApi
36+
public class DpopSigner implements HttpSigner<DpopIdentity> {
37+
38+
@Override
39+
public SignedRequest sign(SignRequest<? extends DpopIdentity> request) {
40+
return SignedRequest.builder()
41+
.request(doSign(request))
42+
.payload(request.payload().orElse(null))
43+
.build();
44+
}
45+
46+
@Override
47+
public CompletableFuture<AsyncSignedRequest> signAsync(AsyncSignRequest<? extends DpopIdentity> request) {
48+
return CompletableFuture.completedFuture(
49+
AsyncSignedRequest.builder()
50+
.request(doSign(request))
51+
.payload(request.payload().orElse(null))
52+
.build());
53+
}
54+
55+
/**
56+
* Using {@link BaseSignRequest}, sign the request with a {@link BaseSignRequest} and re-build it.
57+
*/
58+
private SdkHttpRequest doSign(BaseSignRequest<?, ? extends DpopIdentity> request) {
59+
return request.request().toBuilder()
60+
.putHeader(
61+
"DPoP",
62+
buildDpopHeader(request))
63+
.build();
64+
}
65+
66+
private String buildDpopHeader(BaseSignRequest<?, ? extends DpopIdentity> request) {
67+
SdkHttpRequest httpRequest = request.request();
68+
String endpoint = extractRequestEndpoint(httpRequest);
69+
return DpopHeaderGenerator.generateDPoPProofHeader(
70+
request.identity(), endpoint, httpRequest.method().name(),
71+
Instant.now().getEpochSecond(), UUID.randomUUID().toString());
72+
}
73+
74+
private static String extractRequestEndpoint(SdkHttpRequest httpRequest) {
75+
// using SdkHttpRequest.getUri() results in creating a URI which is slow and we don't need the query components
76+
// construct only the endpoint that we require for DPoP.
77+
String portString =
78+
SdkHttpUtils.isUsingStandardPort(httpRequest.protocol(), httpRequest.port()) ? "" : ":" + httpRequest.port();
79+
return httpRequest.protocol() + "://" + httpRequest.host() + portString + httpRequest.encodedPath();
80+
}
81+
}

0 commit comments

Comments
 (0)