Skip to content

Commit

Permalink
Implement AWS SigV4 authentication (#66)
Browse files Browse the repository at this point in the history
* Implement AWS SigV4 authentication

Signed-off-by: Thomas Farr <[email protected]>

* Improve documentation and ergonomics

Signed-off-by: Thomas Farr <[email protected]>

* Make overloads explicit and document

Signed-off-by: Thomas Farr <[email protected]>

* Fix USER_GUIDE table of contents

Signed-off-by: Thomas Farr <[email protected]>

* Ensure payload checksum is correct when HttpCompression=true on net461

Signed-off-by: Thomas Farr <[email protected]>

* Update packages

Signed-off-by: Thomas Farr <[email protected]>

* Fix license headers

Signed-off-by: Thomas Farr <[email protected]>

* Fix unit test running

Signed-off-by: Thomas Farr <[email protected]>

* Move using outside namespace

Signed-off-by: Thomas Farr <[email protected]>

* Update packages.lock.json

Signed-off-by: Thomas Farr <[email protected]>

Signed-off-by: Thomas Farr <[email protected]>
Signed-off-by: Thomas Farr <[email protected]>
Co-authored-by: Thomas Farr <[email protected]>
Co-authored-by: Kent Chenery <[email protected]>
Co-authored-by: Matt Madigan <[email protected]>
  • Loading branch information
4 people authored Sep 16, 2022
1 parent 7c8a709 commit 364c2f6
Show file tree
Hide file tree
Showing 17 changed files with 1,978 additions and 13 deletions.
13 changes: 13 additions & 0 deletions OpenSearch.sln
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{29E53C13
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSearch.Client.JsonNetSerializer", "src\OpenSearch.Client.JsonNetSerializer\OpenSearch.Client.JsonNetSerializer.csproj", "{B16AAB37-9FF4-4940-AC7D-437DFD18A6F6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSearch.Net.Auth.AwsSigV4", "src\OpenSearch.Net.Auth.AwsSigV4\OpenSearch.Net.Auth.AwsSigV4.csproj", "{2BDA50C8-767A-4560-9547-15AFC972A6E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Auth.AwsSigV4", "tests\Tests.Auth.AwsSigV4\Tests.Auth.AwsSigV4.csproj", "{2BCE8150-C16C-4226-B216-C7A0463ADC56}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSearch.Stack.ArtifactsApiTests", "abstractions\src\OpenSearch.Stack.ArtifactsApiTests\OpenSearch.Stack.ArtifactsApiTests.csproj", "{1F5A7B1A-2566-481F-91B5-A63D7F939973}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "abstractions", "abstractions", "{87ABA679-F3F4-48CE-82B3-1AAE5D0A5935}"
Expand Down Expand Up @@ -121,6 +124,8 @@ Global
{D6997ADC-E933-418E-831C-DE1A78897493} = {29E53C13-34F7-4F0D-8D28-41EF768793E7}
{432D5575-2347-4D3C-BF8C-3E38410C46CA} = {29E53C13-34F7-4F0D-8D28-41EF768793E7}
{B16AAB37-9FF4-4940-AC7D-437DFD18A6F6} = {3EA11364-0513-44B7-AD6D-A675485E7448}
{2BDA50C8-767A-4560-9547-15AFC972A6E3} = {3EA11364-0513-44B7-AD6D-A675485E7448}
{2BCE8150-C16C-4226-B216-C7A0463ADC56} = {6C4A2627-AF22-4388-9DF7-7A9AEACFD635}
{ABE3B7EE-5B31-4C9A-976A-AD28D257B147} = {87ABA679-F3F4-48CE-82B3-1AAE5D0A5935}
{31668B33-6157-4A5B-8D4C-18AF760DCA1B} = {ABE3B7EE-5B31-4C9A-976A-AD28D257B147}
{C80D225C-F072-4B24-9ACE-82EFD9362237} = {ABE3B7EE-5B31-4C9A-976A-AD28D257B147}
Expand Down Expand Up @@ -189,6 +194,14 @@ Global
{B16AAB37-9FF4-4940-AC7D-437DFD18A6F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B16AAB37-9FF4-4940-AC7D-437DFD18A6F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B16AAB37-9FF4-4940-AC7D-437DFD18A6F6}.Release|Any CPU.Build.0 = Release|Any CPU
{2BDA50C8-767A-4560-9547-15AFC972A6E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2BDA50C8-767A-4560-9547-15AFC972A6E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BDA50C8-767A-4560-9547-15AFC972A6E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BDA50C8-767A-4560-9547-15AFC972A6E3}.Release|Any CPU.Build.0 = Release|Any CPU
{2BCE8150-C16C-4226-B216-C7A0463ADC56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2BCE8150-C16C-4226-B216-C7A0463ADC56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BCE8150-C16C-4226-B216-C7A0463ADC56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BCE8150-C16C-4226-B216-C7A0463ADC56}.Release|Any CPU.Build.0 = Release|Any CPU
{1F5A7B1A-2566-481F-91B5-A63D7F939973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F5A7B1A-2566-481F-91B5-A63D7F939973}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F5A7B1A-2566-481F-91B5-A63D7F939973}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down
101 changes: 94 additions & 7 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
- [User Guide](#user-guide)
- [OpenSearch.Client](#opensearchclient)
- [Getting Started](#getting-started)
- [Connecting](#connecting)
- [Indexing](#indexing)
- [Getting a document](#getting-a-document)
- [Searching for documents](#searching-for-documents)
- [Falling back to OpenSearch.Net](#falling-back-to-opensearchnet)
- [OpenSearch.Net](#opensearchnet)
- [OpenSearch.Net.Auth.AwsSigV4](#opensearchnetauthawssigv4)
- [Getting Started](#getting-started-1)
- [Connecting](#connecting-1)
- [Configuring Region & Credentials](#configuring-region--credentials)
- [OpenSearch.Net](#opensearchnet)
- [Getting Started](#getting-started-2)
- [Connecting](#connecting-2)
# User Guide

This user guide specifies how to include and use the .NET client in your application.

## [OpenSearch.Client](src/OpenSearch.Client)

### Getting Started

Include OSC in your .csproj file.
Include OpenSearch.Client in your .csproj file.
```xml
<Project>
...
<ItemGroup>
<PackageReference Include="Osc" Version="1.0.0" />
<PackageReference Include="OpenSearch.Client" Version="1.0.0" />
</ItemGroup>
</Project>
```
Expand All @@ -31,7 +38,7 @@ You can connect to your OpenSearch cluster via a single node, or by specifying m
**Connecting to a single node**
```csharp
var node = new Uri("http://myserver:9200");
var config = new ConnectionConfiguration(node);
var config = new ConnectionSettings(node);
var client = new OpenSearchClient(config);
```

Expand Down Expand Up @@ -83,7 +90,7 @@ var tweet = getResponse.Source; // the original document

### Searching for documents

OSC exposes a fluent interface and a [powerful query DSL](https://opensearch.org/docs/latest/opensearch/query-dsl/index/)
OpenSearch.Client exposes a fluent interface and a [powerful query DSL](https://opensearch.org/docs/latest/opensearch/query-dsl/index/)

```csharp
var searchResponse = client.Search<Tweet>(s => s
Expand Down Expand Up @@ -112,7 +119,7 @@ var searchResponse = client.Search<Tweet>(request);
```
### Falling back to OpenSearch.Net

OSC also includes and exposes the low-level [OpenSearch.Net](https://github.com/opensearch-project/opensearch-net/tree/main/src/OpenSearch.Net) client that you can fall back to in case anything is missing:
OpenSearch.Client also includes and exposes the low-level [OpenSearch.Net](https://github.com/opensearch-project/opensearch-net/tree/main/src/OpenSearch.Net) client that you can fall back to in case anything is missing:

```csharp
IOpenSearchLowLevelClient lowLevelClient = client.LowLevel;
Expand All @@ -132,6 +139,86 @@ var response = lowLevelClient.Search<SearchResponse<Tweet>>("mytweetindex", Post
}));
```

## [OpenSearch.Net.Auth.AwsSigV4](src/OpenSearch.Net.Auth.AwsSigV4)

An implementation of AWS SigV4 request signing for performing IAM authentication against the managed Amazon OpenSearch Service.
It can be used with both the low-level OpenSearch.Net client as well as the higher-level OpenSearch.Client client.

### Getting Started
Include OpenSearch.Net.Auth.AwsSigV4 along with your preferred client in your .csproj file.
```xml
<Project>
...
<ItemGroup>
<PackageReference Include="OpenSearch.Client" Version="1.0.0" />
<PackageReference Include="OpenSearch.Net.Auth.AwsSigV4" Version="1.0.0" />
</ItemGroup>
</Project>
```

### Connecting
The only wiring required is to use the `AwsSigV4HttpConnection` implementation of `IConnection`.

**With OpenSearch.Client**
```csharp
var endpoint = new Uri("https://example-aaabbbcccddd111222333.us-east-1.es.amazonaws.com");
var connection = new AwsSigV4HttpConnection();
var config = new ConnectionSettings(endpoint, connection);
var client = new OpenSearchClient(config);
```

**With OpenSearch.Net**
```csharp
var endpoint = new Uri("https://example-aaabbbcccddd111222333.us-east-1.es.amazonaws.com");
var connection = new AwsSigV4HttpConnection();
var config = new ConnectionConfiguration(endpoint, connection);
var client = new OpenSearchLowLevelClient(config);
```

### Configuring Region & Credentials
By default, `AwsSigV4HttpConnection` will use the same default logic as the [AWS SDK for .NET](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/creds-assign.html) to determine the credentials and region to use.
However, you may explicitly specify one or both to override this logic, for example:

**Explicitly setting the region, but the credentials coming from the environment**
```shell
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
```
```csharp
var endpoint = new Uri("https://example-aaabbbcccddd111222333.ap-southeast-2.es.amazonaws.com");
var connection = new AwsSigV4HttpConnection(RegionEndpoint.APSoutheast2);
var config = new ConnectionSettings(endpoint, connection);
var client = new OpenSearchClient(config);
```

**Explicitly setting credentials, such as to assume a role, but the region coming from the environment**
```shell
export AWS_REGION="ap-southeast-2"
```
```csharp
var endpoint = new Uri("https://example-aaabbbcccddd111222333.ap-southeast-2.es.amazonaws.com");
var credentials = new AssumeRoleAWSCredentials(
FallbackCredentialsFactory.GetCredentials(),
"arn:aws:iam::123456789012:role/my-open-search-ingest-role",
"my-ingest-application");
var connection = new AwsSigV4HttpConnection(credentials);
var config = new ConnectionSettings(endpoint, connection);
var client = new OpenSearchClient(config);
```

**Explicitly setting credentials and region, such as to assume a role**
```csharp
var endpoint = new Uri("https://example-aaabbbcccddd111222333.ap-southeast-2.es.amazonaws.com");
var credentials = new AssumeRoleAWSCredentials(
FallbackCredentialsFactory.GetCredentials(),
"arn:aws:iam::123456789012:role/my-open-search-ingest-role",
"my-ingest-application");
var connection = new AwsSigV4HttpConnection(credentials, RegionEndpoint.APSoutheast2);
var config = new ConnectionSettings(endpoint, connection);
var client = new OpenSearchClient(config);
```

## [OpenSearch.Net](src/OpenSearch.Net)

A low-level, dependency free client that is a simple .NET wrapper for the REST API. It allows you to build and represent your own requests and responses according to you needs.
Expand All @@ -149,7 +236,7 @@ Include OpenSearch.Net in your .csproj file.

### Connecting

Connecting using the low-level client is very similar to how you would connect using OSC. In fact, the connection constructs that OSC use are actually OpenSearch.Net constructs. Thus, single node connections and connection pooling still apply when using OpenSearch.Net.
Connecting using the low-level client is very similar to how you would connect using OpenSearch.Client. In fact, the connection constructs that OpenSearch.Client use are actually OpenSearch.Net constructs. Thus, single node connections and connection pooling still apply when using OpenSearch.Net.

```csharp
var node = new Uri("http://myserver:9200");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ public class ConnectionSettings : ConnectionSettingsBase<ConnectionSettings>
/// Creates a new instance of connection settings, if <paramref name="uri"/> is not specified will default to connecting to http://localhost:9200
/// </summary>
/// <param name="uri"></param>
public ConnectionSettings(Uri uri = null) : this(new SingleNodeConnectionPool(uri ?? new Uri("http://localhost:9200"))) { }
/// <param name="connection"></param>
public ConnectionSettings(Uri uri = null, IConnection connection = null) : this(new SingleNodeConnectionPool(uri ?? new Uri("http://localhost:9200")), connection) { }

/// <summary>
/// Sets up the client to communicate to OpenSearch Cloud using <paramref name="cloudId"/>,
Expand Down
42 changes: 42 additions & 0 deletions src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpClientHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

#if DOTNETCORE

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Amazon;
using Amazon.Runtime;

namespace OpenSearch.Net.Auth.AwsSigV4
{
internal class AwsSigV4HttpClientHandler : DelegatingHandler
{
private readonly AWSCredentials _credentials;
private readonly RegionEndpoint _region;

public AwsSigV4HttpClientHandler(AWSCredentials credentials, RegionEndpoint region, HttpMessageHandler innerHandler)
: base(innerHandler)
{
_credentials = credentials ?? throw new ArgumentNullException(nameof(credentials));
_region = region ?? throw new ArgumentNullException(nameof(region));
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var credentials = await _credentials.GetCredentialsAsync().ConfigureAwait(false);

await AwsSigV4Util.SignRequest(request, credentials, _region, DateTime.UtcNow).ConfigureAwait(false);

return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
}

#endif
76 changes: 76 additions & 0 deletions src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

using System;
using Amazon;
using Amazon.Runtime;

namespace OpenSearch.Net.Auth.AwsSigV4
{
/// <summary>
/// An <see cref="IConnection"/> implementation that performs AWS SigV4 request signing, for performing authentication with Amazon Managed OpenSearch.
/// </summary>
public class AwsSigV4HttpConnection : HttpConnection
{
private readonly AWSCredentials _credentials;
private readonly RegionEndpoint _region;

/// <summary>
/// Construct a new connection discovering both the credentials and region from the environment.
/// </summary>
/// <seealso cref="AwsSigV4HttpConnection(AWSCredentials, RegionEndpoint)"/>
public AwsSigV4HttpConnection() : this(null, null) { }

/// <summary>
/// Construct a new connection configured with the specified credentials and using the region discovered from the environment.
/// </summary>
/// <param name="credentials">The credentials to use when signing.</param>
/// <seealso cref="AwsSigV4HttpConnection(AWSCredentials, RegionEndpoint)"/>
public AwsSigV4HttpConnection(AWSCredentials credentials) : this(credentials, null) { }

/// <summary>
/// Construct a new connection configured with a specified region and using credentials discovered from the environment.
/// </summary>
/// <param name="region">The region to use when signing.</param>
/// <seealso cref="AwsSigV4HttpConnection(AWSCredentials, RegionEndpoint)"/>
public AwsSigV4HttpConnection(RegionEndpoint region) : this(null, region) { }

/// <summary>
/// Construct a new connection configured with the given credentials and region.
/// </summary>
/// <param name="credentials">The credentials to use when signing, or null to have them discovered automatically by the AWS SDK.</param>
/// <param name="region">The region to use when signing, or null to have it discovered automatically by the AWS SDK.</param>
/// <exception cref="ArgumentNullException">Thrown if region is null and is unable to be automatically discovered by the AWS SDK.</exception>
/// <remarks>
/// The same logic as the <a href="https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/creds-assign.html">AWS SDK for .NET</a>
/// is used to automatically discover the credentials and region to use if not provided explicitly.
/// </remarks>
public AwsSigV4HttpConnection(AWSCredentials credentials, RegionEndpoint region)
{
_credentials = credentials ?? FallbackCredentialsFactory.GetCredentials(); // FallbackCredentialsFactory throws in case of not finding credentials.
_region = region
?? FallbackRegionFactory.GetRegionEndpoint() // FallbackRegionFactory can return null.
?? throw new ArgumentNullException(nameof(region), "A RegionEndpoint was not provided and was unable to be determined from the environment.");
}

#if DOTNETCORE

protected override System.Net.Http.HttpMessageHandler CreateHttpClientHandler(RequestData requestData) =>
new AwsSigV4HttpClientHandler(_credentials, _region, base.CreateHttpClientHandler(requestData));

#else

protected override System.Net.HttpWebRequest CreateHttpWebRequest(RequestData requestData)
{
var request = base.CreateHttpWebRequest(requestData);
AwsSigV4Util.SignRequest(request, requestData, _credentials.GetCredentials(), _region, DateTime.UtcNow);
return request;
}

#endif
}
}
59 changes: 59 additions & 0 deletions src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4Util.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

using System;
#if DOTNETCORE
using System.Net.Http;
using System.Threading.Tasks;
#else
using System.Net;
#endif
using Amazon;
using Amazon.Runtime;
using Amazon.Runtime.Internal.Auth;

namespace OpenSearch.Net.Auth.AwsSigV4
{
public static class AwsSigV4Util
{
#if DOTNETCORE
public static async Task SignRequest(
HttpRequestMessage request,
ImmutableCredentials credentials,
RegionEndpoint region,
DateTime signingTime)
{
var canonicalRequest = await CanonicalRequest.From(request, credentials, signingTime).ConfigureAwait(false);

var signature = AWS4Signer.ComputeSignature(credentials, region.SystemName, signingTime, "es", canonicalRequest.SignedHeaders,
canonicalRequest.ToString());

request.Headers.TryAddWithoutValidation("x-amz-date", canonicalRequest.XAmzDate);
request.Headers.TryAddWithoutValidation("authorization", signature.ForAuthorizationHeader);
if (!string.IsNullOrEmpty(canonicalRequest.XAmzSecurityToken)) request.Headers.TryAddWithoutValidation("x-amz-security-token", canonicalRequest.XAmzSecurityToken);
}
#else
public static void SignRequest(
HttpWebRequest request,
RequestData requestData,
ImmutableCredentials credentials,
RegionEndpoint region,
DateTime signingTime)
{
var canonicalRequest = CanonicalRequest.From(request, requestData, credentials, signingTime);

var signature = AWS4Signer.ComputeSignature(credentials, region.SystemName, signingTime, "es", canonicalRequest.SignedHeaders,
canonicalRequest.ToString());

request.Headers["x-amz-date"] = canonicalRequest.XAmzDate;
request.Headers["authorization"] = signature.ForAuthorizationHeader;
if (!string.IsNullOrEmpty(canonicalRequest.XAmzSecurityToken))
request.Headers["x-amz-security-token"] = canonicalRequest.XAmzSecurityToken;
}
#endif
}
}
Loading

0 comments on commit 364c2f6

Please sign in to comment.