Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions runtime/auth/aws-signing-common/api/aws-signing-common.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
public final class aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator {
public fun <init> (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 <init> (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 <init> (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 <init> (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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = [email protected]
signingDate = clock.now()
expiresAfter = expiration
signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS
}

return signer.sign(req, config).output.url.trimScheme()
}
}
Original file line number Diff line number Diff line change
@@ -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<HttpRequest> {
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<HttpRequest>(builder.build(), "signature".encodeToByteArray())
}

override suspend fun signChunk(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult<Unit> = throw IllegalStateException("signChunk unexpectedly invoked")

override suspend fun signChunkTrailer(trailingHeaders: Headers, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult<Unit> = throw IllegalStateException("signChunkTrailer unexpectedly invoked")
}
Loading