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..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,3 +1,13 @@ +public final class aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator { + 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 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 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..e92ac1f72c --- /dev/null +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -0,0 +1,44 @@ +/* +* 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.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 kotlin.time.Duration + +/** + * 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 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 signer: AwsSigner, + public val clock: Clock = Clock.System, +) { + private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://") + + public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { + val req = HttpRequest(HttpMethod.GET, endpoint) + + val config = AwsSigningConfig { + credentials = credentialsProvider.resolve() + this.region = region + service = this@AuthTokenGenerator.service + signingDate = clock.now() + expiresAfter = expiration + signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS + } + + return signer.sign(req, config).output.url.trimScheme() + } +} 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..87fec1a74e --- /dev/null +++ b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt @@ -0,0 +1,71 @@ +/* +* 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") +}