From 179c1c0394fc53fd0523e50b61f66ad7716528f6 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 9 Jan 2025 11:39:35 -0500 Subject: [PATCH 1/6] Relocate AuthTokenGenerator to aws-signing-common --- .../auth/awssigning/AuthTokenGenerator.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt new file mode 100644 index 0000000000..4c1912b1d6 --- /dev/null +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -0,0 +1,63 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Clock +import aws.smithy.kotlin.runtime.util.ExpiringValue +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +// The default expiration value to use for [Credentials] when none is provided. +private val DEFAULT_CREDENTIALS_EXPIRATION = 10.minutes + +/** + * Generates an authentication token, which is a SigV4-signed URL with the HTTP scheme removed. + * @param service The name of the service the token is being generated for + * @param credentialsProvider The [CredentialsProvider] which will provide credentials to use when generating the auth token + * @param credentialsRefreshBuffer The amount of time before the resolved [Credentials] expire in which they are considered expired, defaults to 10 seconds. + * @param signer The [AwsSigner] implementation to use when creating the authentication token + * @param clock The [Clock] implementation to use + */ +public class AuthTokenGenerator( + public val service: String, + public val credentialsProvider: CredentialsProvider, + public val credentialsRefreshBuffer: Duration = 10.seconds, + public val signer: AwsSigner, + public val clock: Clock = Clock.System, +) { + private lateinit var credentials: ExpiringValue + + private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://") + + public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { + if (!::credentials.isInitialized || (credentials.expiresAt - clock.now()).absoluteValue <= credentialsRefreshBuffer) { + val resolved = credentialsProvider.resolve() + credentials = ExpiringValue(resolved, resolved.expiration ?: (clock.now() + DEFAULT_CREDENTIALS_EXPIRATION)) + } + + val req = HttpRequest(HttpMethod.GET, endpoint) + + val creds = credentials.value + val serv = service + + val config = AwsSigningConfig { + credentials = creds + this.region = region + service = serv + signingDate = clock.now() + expiresAfter = expiration + signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS + } + + return signer.sign(req, config).output.url.trimScheme() + } +} From 6e3c0fdd8167586cbae9ef1edfa16fe8e7218757 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 9 Jan 2025 11:40:29 -0500 Subject: [PATCH 2/6] apiDump --- .../aws-signing-common/api/aws-signing-common.api | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/runtime/auth/aws-signing-common/api/aws-signing-common.api b/runtime/auth/aws-signing-common/api/aws-signing-common.api index 0c96486679..77b8b7ec50 100644 --- a/runtime/auth/aws-signing-common/api/aws-signing-common.api +++ b/runtime/auth/aws-signing-common/api/aws-signing-common.api @@ -1,3 +1,14 @@ +public final class aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator { + public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;JLaws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;JLaws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun generateAuthToken-exY8QGI (Laws/smithy/kotlin/runtime/net/url/Url;Ljava/lang/String;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getClock ()Laws/smithy/kotlin/runtime/time/Clock; + public final fun getCredentialsProvider ()Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider; + public final fun getCredentialsRefreshBuffer-UwyO8pc ()J + public final fun getService ()Ljava/lang/String; + public final fun getSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; +} + public final class aws/smithy/kotlin/runtime/auth/awssigning/AwsChunkedByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel { public fun (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;)V public synthetic fun (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;ILkotlin/jvm/internal/DefaultConstructorMarker;)V From ba50bdf882b9008f9db929fb3000d7998ae71565 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 9 Jan 2025 16:54:56 -0500 Subject: [PATCH 3/6] Call `credentialsProvider.resolve()` for each generation --- .../runtime/auth/awssigning/AuthTokenGenerator.kt | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt index 4c1912b1d6..6703ade792 100644 --- a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -16,9 +16,6 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -// The default expiration value to use for [Credentials] when none is provided. -private val DEFAULT_CREDENTIALS_EXPIRATION = 10.minutes - /** * Generates an authentication token, which is a SigV4-signed URL with the HTTP scheme removed. * @param service The name of the service the token is being generated for @@ -30,27 +27,18 @@ private val DEFAULT_CREDENTIALS_EXPIRATION = 10.minutes public class AuthTokenGenerator( public val service: String, public val credentialsProvider: CredentialsProvider, - public val credentialsRefreshBuffer: Duration = 10.seconds, public val signer: AwsSigner, public val clock: Clock = Clock.System, ) { - private lateinit var credentials: ExpiringValue - private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://") public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { - if (!::credentials.isInitialized || (credentials.expiresAt - clock.now()).absoluteValue <= credentialsRefreshBuffer) { - val resolved = credentialsProvider.resolve() - credentials = ExpiringValue(resolved, resolved.expiration ?: (clock.now() + DEFAULT_CREDENTIALS_EXPIRATION)) - } - val req = HttpRequest(HttpMethod.GET, endpoint) - val creds = credentials.value val serv = service val config = AwsSigningConfig { - credentials = creds + credentials = credentialsProvider.resolve() this.region = region service = serv signingDate = clock.now() From 73eccb309bbe1345ae29896f79a044ad0cce0969 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 9 Jan 2025 17:14:01 -0500 Subject: [PATCH 4/6] Remove caching logic, add a test --- .../api/aws-signing-common.api | 5 +- .../auth/awssigning/AuthTokenGenerator.kt | 5 -- .../auth/awssigning/AuthTokenGeneratorTest.kt | 77 +++++++++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt diff --git a/runtime/auth/aws-signing-common/api/aws-signing-common.api b/runtime/auth/aws-signing-common/api/aws-signing-common.api index 77b8b7ec50..d128b0bd6f 100644 --- a/runtime/auth/aws-signing-common/api/aws-signing-common.api +++ b/runtime/auth/aws-signing-common/api/aws-signing-common.api @@ -1,10 +1,9 @@ public final class aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator { - public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;JLaws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;JLaws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;)V + public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun generateAuthToken-exY8QGI (Laws/smithy/kotlin/runtime/net/url/Url;Ljava/lang/String;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getClock ()Laws/smithy/kotlin/runtime/time/Clock; public final fun getCredentialsProvider ()Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider; - public final fun getCredentialsRefreshBuffer-UwyO8pc ()J public final fun getService ()Ljava/lang/String; public final fun getSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; } diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt index 6703ade792..fd95d1e95d 100644 --- a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -4,23 +4,18 @@ */ package aws.smithy.kotlin.runtime.auth.awssigning -import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke import aws.smithy.kotlin.runtime.http.HttpMethod import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.net.url.Url import aws.smithy.kotlin.runtime.time.Clock -import aws.smithy.kotlin.runtime.util.ExpiringValue import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes -import kotlin.time.Duration.Companion.seconds /** * Generates an authentication token, which is a SigV4-signed URL with the HTTP scheme removed. * @param service The name of the service the token is being generated for * @param credentialsProvider The [CredentialsProvider] which will provide credentials to use when generating the auth token - * @param credentialsRefreshBuffer The amount of time before the resolved [Credentials] expire in which they are considered expired, defaults to 10 seconds. * @param signer The [AwsSigner] implementation to use when creating the authentication token * @param clock The [Clock] implementation to use */ diff --git a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt new file mode 100644 index 0000000000..21c145dd0d --- /dev/null +++ b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt @@ -0,0 +1,77 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.collections.Attributes +import aws.smithy.kotlin.runtime.http.Headers +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.net.Host +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.time.ManualClock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit + +class AuthTokenGeneratorTest { + @Test + fun testGenerateAuthToken() = runTest { + val credentials = Credentials("akid", "secret") + + val credentialsProvider = object : CredentialsProvider { + var credentialsResolved = false + override suspend fun resolve(attributes: Attributes): Credentials { + credentialsResolved = true + return credentials + } + } + + val clock = ManualClock(Instant.fromEpochSeconds(0)) + + val generator = AuthTokenGenerator("foo", credentialsProvider, TEST_SIGNER, clock = clock) + + + val endpoint = Url { host = Host.parse("foo.bar.us-east-1.baz") } + val token = generator.generateAuthToken(endpoint, "us-east-1", 333.seconds) + + assertContains(token, "foo.bar.us-east-1.baz") + assertContains(token, "X-Amz-Credential=signature") // test custom signer was invoked + assertContains(token, "X-Amz-Expires=333") // expiration + assertContains(token, "X-Amz-SigningDate=0") // clock + + assertTrue(credentialsProvider.credentialsResolved) + } +} + +private val TEST_SIGNER = object : AwsSigner { + override suspend fun sign( + request: HttpRequest, + config: AwsSigningConfig, + ): AwsSigningResult { + val builder = request.toBuilder() + builder.url.parameters.decodedParameters.apply { + put("X-Amz-Credential", "signature") + put("X-Amz-Expires", (config.expiresAfter?.toLong(DurationUnit.SECONDS) ?: 900).toString()) + put("X-Amz-SigningDate", config.signingDate.epochSeconds.toString()) + } + + return AwsSigningResult(builder.build(), "signature".encodeToByteArray()) + } + + override suspend fun signChunk(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult { + throw IllegalStateException("signChunk unexpectedly invoked") + } + + override suspend fun signChunkTrailer(trailingHeaders: Headers, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult { + throw IllegalStateException("signChunkTrailer unexpectedly invoked") + } + +} \ No newline at end of file From 4aae969c90917d544daf54c611939dfe8832d6e7 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 9 Jan 2025 17:19:20 -0500 Subject: [PATCH 5/6] ktlint --- .../auth/awssigning/AuthTokenGeneratorTest.kt | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt index 21c145dd0d..87fec1a74e 100644 --- a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt +++ b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt @@ -38,7 +38,6 @@ class AuthTokenGeneratorTest { val generator = AuthTokenGenerator("foo", credentialsProvider, TEST_SIGNER, clock = clock) - val endpoint = Url { host = Host.parse("foo.bar.us-east-1.baz") } val token = generator.generateAuthToken(endpoint, "us-east-1", 333.seconds) @@ -66,12 +65,7 @@ private val TEST_SIGNER = object : AwsSigner { return AwsSigningResult(builder.build(), "signature".encodeToByteArray()) } - override suspend fun signChunk(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult { - throw IllegalStateException("signChunk unexpectedly invoked") - } + override suspend fun signChunk(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult = throw IllegalStateException("signChunk unexpectedly invoked") - override suspend fun signChunkTrailer(trailingHeaders: Headers, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult { - throw IllegalStateException("signChunkTrailer unexpectedly invoked") - } - -} \ No newline at end of file + override suspend fun signChunkTrailer(trailingHeaders: Headers, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult = throw IllegalStateException("signChunkTrailer unexpectedly invoked") +} From 52dbd28d48328d70a056689ce7a42e1b4fc6130f Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 10 Jan 2025 10:19:18 -0500 Subject: [PATCH 6/6] `this@AuthTokenGenerator.service` --- .../kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt index fd95d1e95d..e92ac1f72c 100644 --- a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -30,12 +30,10 @@ public class AuthTokenGenerator( public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { val req = HttpRequest(HttpMethod.GET, endpoint) - val serv = service - val config = AwsSigningConfig { credentials = credentialsProvider.resolve() this.region = region - service = serv + service = this@AuthTokenGenerator.service signingDate = clock.now() expiresAfter = expiration signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS