awslabs / aws-sdk-kotlin

Multiplatform AWS SDK for Kotlin
Apache License 2.0
412 stars 49 forks source link

Public API for URL signing? #999

Open trevjonez opened 1 year ago

trevjonez commented 1 year ago

Describe the issue

Currently trying to build an ApiGateway websocket driven app that is using IAM for auth and need a way to sign a wss URL in order to connect.

Steps to Reproduce

This SO answer gives a good description of what I am wanting to achieve.

I have got an answer from AWS support. I will need to sign the wss URL. So instead of setting request headers in a HTTP request, the signature information will be passed to the url in the query string parameters. A signed wss URL looks like: wss://API_ID.execute- api.region.amazonaws.com/dev?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz- Credential=ACCESSKEY/20200131/region/execute-api/aws4_request&X-Amz-Date=20200131T100233Z&X- Amz-Security-Token=SECURITY_TOKEN&X-Amz-SignedHeaders=host&X-Amz-Signature=SIGNATURE.

To generate the signed URL, I can use Signer.signUrl method from @aws-amplify/core library.

Current behavior

All of the signing details are buried in the Smithy SDK as an implementation detail of the things that provide pre-signing options. (IE: S3)

AWS Kotlin SDK version used

0.29.0-beta

Platform (JVM/JS/Native)

JVM

Operating System and version

N/A

lauzadis commented 1 year ago

Thanks for the question. Can you share more information about the specific SDK operation you're trying to create a presigned URL for? You're correct that we don't expose pre-signing capabilities and instead generate them for a few specific services. We may be able to add it for API Gateway depending on the use case.

trevjonez commented 1 year ago

My usecase is I believe very similar to the SO answer.

I want to sign a wss://API_ID.execute-api.REGION.amazonaws.com/STAGE?potentiallyWithQueryArgs=true URL so that I can use AWS_IAM as the auth type.

In my case it will be from the JVM most likely using OKHttp as the client implementation.

Current best looking workaround I think will be using the com.amazonaws.auth.AWS4Signer from the android aws core sdk. But I do think having some sort of public API similar to what the android sdk provides would be appropriate for the kotlin SDK.

lauzadis commented 8 months ago

Hi and sorry for the delay in our response.

This is already somewhat possible with the Kotlin SDK, here is an example. You will need to @OptIn(InternalApi::class) to call signer.sign(...). We will discuss as a team whether that API should be made public.

val signer = DefaultAwsSigner

val parsedUrl = Url.parse("wss://$API_ID.execute-api.$REGION.amazonaws.com/$STAGE")
val req = HttpRequest(method = HttpMethod.GET, url = parsedUrl)

val credentialsProvider = // your AWS credentials provider
val signingConfig = AwsSigningConfig {
    algorithm = AwsSigningAlgorithm.SIGV4
    signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS
    credentials = credentialsProvider.resolve()
    region = "us-west-2"
    service = "execute-api"
}

val signedUrl = signer.sign(req, signingConfig).output.url
// use signedUrl as needed...

What sort of things would you like to see changed to make it easier to sign URLs?

cloudshiftchris commented 7 months ago

There are a few AWS services, such as Lambda function URLs, that aren't an AWS SDK call - they're an HTTPS endpoint that requires Sigv4 for AWS_IAM auth.

Struggling to see how to use the Kotlin AWS SDK for this - it isn't a general-purpose "HTTP Client". Its often preferable to use other Http clients to make those REST/HTTPS/whatever requests (there are many other non-SDK considerations - marshalling request/response payloads, etc) - but the sensitive logic on signing is baked into the SDK code, assumes that the request will be made by the SDK HTTP client. One could, of course, re-implement the signing logic, though that seems fragile and not an effective use of time when it already exists(ish).

Perhaps the signing logic could be decoupled from the HttpRequest such that is can be used elsewhere?

lauzadis commented 7 months ago

I think HttpRequest is the correct abstraction for signing. It is formed by an HTTP method, URL endpoint, and optional request headers / body. You can create an HttpRequest from a Lambda function URL and then sign it by doing something similar to my example code above.

After signing, if you don't want to use our SDK's HTTP client to complete the request, you can convert the request to your desired HTTP client's request type.

Do you have a different idea of what a decoupled signer would look like? What should it take as input?

cloudshiftchris commented 7 months ago

A few thoughts now that we managed to make this work:

1) It isn't clear that signing, specifically DefaultAwsSigner, is intended to be part of the SDK public API; its from an ancillary aws.smithy.kotlin:aws-signing-default dependency, which makes if fuzzy as to whether its stable for consumption (or exposed for consumption by the SDK). It comes across as "hey, we found this random transitive dependency and made it work" rather than a supported part of the API. 2) DefaultAwsSigner changes logging behaviour when used from SDK vs standalone; when used standalone there is no telemetryProvider registered, hence no logging provider, hence no debug/trace log output emitted; 3) There doesn't appear to be a way to have an unsigned payload - the code expects a pre-calculated hash, or hashes the payload; 4) The shouldSignHeader predicate in AwsSigningConfig is poorly documented: The default predicate is to not reject signing any headers (i. e., _ -> true). - double negative, "reject" is odd. Perhaps something like "The default predicate signs all headers".

A documentation page on patterns for manually signing requests, e.g. lambda function URLs etc, would go a long way here.

lauzadis commented 7 months ago

@cloudshiftchris Thanks for the detailed feedback. We've added some backlog tasks to clean up the documentation and improve functionality when using the signer standalone.