Skip to content

Commit

Permalink
1.0.0 - initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
KlemenKozelj committed Aug 5, 2023
0 parents commit bd82242
Show file tree
Hide file tree
Showing 18 changed files with 908 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: CI

on: [push]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.20.x'
- run: go mod download
- run: go install github.com/securego/gosec/v2/cmd/gosec@latest
- run: gosec ./...
- run: go test ./...
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
coverage.out
coverage.html
13 changes: 13 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"cSpell.words": [
"AKIATESTACCESSKEY",
"Fprintln",
"gosec",
"Passwordless",
"rolename",
"securego",
"useast",
"uswest",
"YYYYMMDDTHHMMSSZ"
]
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2023-08-05
### Added
- initial implementation
20 changes: 20 additions & 0 deletions MIT-LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2023 Klemen Kozelj

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# go-aws-sts-authenticator

[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) ![CI](https://github.com/KlemenKozelj/go-aws-sts-authenticator/actions/workflows/main.yml/badge.svg) ![Vulnerabilities](https://snyk.io/test/github/KlemenKozelj/go-aws-sts-authenticator/badge.svg)

**go-aws-sts-authenticator** is a lightweight open-source project developed in Go. The primary objective of this project is to simplify API request authentication by reusing AWS IAM and STS services. It was inspired by Mongo's Atlas password-less database authentication mechanism, please see the credit section below for details.

### Install
```
go get github.com/klemenkozelj/go-aws-sts-authenticator
```

### Usage
![](assets/example-flow.gif)
[Script](example/main.go) for demonstrative execution that showcases how STS parameters retrieval on client its verification on server:
```
git clone https://github.com/KlemenKozelj/go-aws-sts-authenticator.git
cd go-aws-sts-authenticator/example
go run main.go // AWS IAM ARN = arn:aws:iam::123456789012:user/klemen.kozelj@gmail.com
```
#### On Client
Client needs to have configured AWS IAM identity (user or role).
```golang
// NewClient creates a new StsAuthenticator client with default AWS environment configuration.
signatureClient, err := client.NewClient(ctx)

// Generates STS temporary credentials and from them derives authentication parameters.
xAmzDate, authorization, xAmzSecurityToken, awsRegion, err := signatureClient.GetStsParameters(ctx)

// We can also directly sign http.Request and parameters will be attached to request headers.
err := signatureClient.SignRequest(req)
```

#### On Server
The server is calling the public AWS STS (Security Token Service) endpoint with the provided authentication parameters and does not require any AWS configuration.
```golang
// Validates authentication parameters, if successful callers IAM ARN identity is located in awsGetCallerIdentityResponse.GetCallerIdentityResult.Arn
awsIdentity, err := server.StsGetCallerIdentity(xAmzDate, authorization, xAmzSecurityToken, awsRegion)
```
Validation is most frequently done in the context of an HTTP web server, http.Handler middleware functions are included for this purpose:
```golang
mux := http.NewServeMux()

// Request is authenticated if callers AWS IAM ARN is equal to arn:aws:iam::1234567890:user/username
stsAuthenticationMiddleware := server.AuthenticateAwsIamIdentity(
server.DefaultGetRequestParameters, // type GetRequestParameters func(r *http.Request) (awsRegion, xAmzDate, xAmzSecurityToken, authorization string, err error)
server.DefaultIsIamIdentityValid("arn:aws:iam::1234567890:user/username"), // type IsIamIdentityValid func(awsIamIdentityArn string) (valid bool)
)

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Hello World")
})

mux.Handle("/", stsAuthenticationMiddleware(handler))
```
### External Dependencies
- [aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2)

### Potential Improvements
- [ ] improve code testability with mocking aws-sdk-go-v2
- [ ] tests for client.go
- [ ] exponential back-off retry strategy for server's http client
- [ ] possibility to insert custom server's http client
- [ ] in-code documentation, benchmark and examples

### Credits

Mongo's Atlas password-less database authentication mechanism:
- [Set Up Passwordless Authentication with AWS IAM](https://www.mongodb.com/docs/atlas/security/passwordless-authentication/)
- [Using AWS IAM Authentication with MongoDB 4.4 in Atlas to Build Modern Secure Applications](https://www.youtube.com/watch?v=99iV9lCctrU)

Scanned with:
- [GoSec - Golang Security Checker](https://github.com/securego/gosec)
Binary file added assets/example-flow.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
93 changes: 93 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package client

import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sts"
)

const body string = "Action=GetCallerIdentity&Version=2011-06-15"

var requestBodySha256 string = getSHA256Hash(body)

type StsAuthenticator struct {
awsRegion string
signer *v4.Signer
stsClient *sts.Client
}

func NewClient(ctx context.Context) (*StsAuthenticator, error) {
client := StsAuthenticator{
signer: v4.NewSigner(),
}
configuration, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, err
}
client.awsRegion = configuration.Region
client.stsClient = sts.NewFromConfig(configuration)
return &client, nil
}

func (c StsAuthenticator) getTemporarilyAwsCredentials(ctx context.Context) (*aws.Credentials, error) {
result, err := c.stsClient.GetSessionToken(ctx, &sts.GetSessionTokenInput{})
if err != nil {
return nil, err
}
awsCredentials := credentials.NewStaticCredentialsProvider(
*result.Credentials.AccessKeyId,
*result.Credentials.SecretAccessKey,
*result.Credentials.SessionToken,
)
credentials, err := awsCredentials.Retrieve(ctx)
if err != nil {
return nil, err
}
return &credentials, nil
}

func (c StsAuthenticator) GetStsParameters(ctx context.Context) (xAmzDate, authorization, xAmzSecurityToken, awsRegion string, err error) {
awsCredentials, err := c.getTemporarilyAwsCredentials(ctx)
if err != nil {
return "", "", "", "", err
}
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://sts.%s.amazonaws.com", c.awsRegion), strings.NewReader(body))
if err != nil {
return "", "", "", "", err
}
err2 := c.signer.SignHTTP(ctx, *awsCredentials, req, requestBodySha256, "sts", c.awsRegion, time.Now())
if err2 != nil {
return "", "", "", "", err2
}
return req.Header.Get("X-Amz-Date"), req.Header.Get("Authorization"), req.Header.Get("X-Amz-Security-Token"), c.awsRegion, nil
}

func (c StsAuthenticator) SignRequest(req *http.Request) error {
awsCredentials, err := c.getTemporarilyAwsCredentials(req.Context())
if err != nil {
return err
}
err2 := c.signer.SignHTTP(req.Context(), *awsCredentials, req, requestBodySha256, "sts", c.awsRegion, time.Now())
if err2 != nil {
return err2
}
return nil
}

func getSHA256Hash(input string) string {
hash := sha256.New()
hash.Write([]byte(input))
hashBytes := hash.Sum(nil)
hashString := hex.EncodeToString(hashBytes)
return hashString
}
60 changes: 60 additions & 0 deletions example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"context"
"fmt"
"net/http"

"github.com/klemenkozelj/go-aws-http-authenticator/client"
"github.com/klemenkozelj/go-aws-http-authenticator/server"
)

func main() {

xAmzDate, authorization, xAmzSecurityToken, awsRegion := clientExample()

serverExample(xAmzDate, authorization, xAmzSecurityToken, awsRegion)

mux := http.NewServeMux()

awsAuth := server.AuthenticateAwsIamIdentity(
server.DefaultGetRequestParameters,
server.DefaultIsIamIdentityValid("arn:aws:iam::1234567890:user/username"),
)

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Your handler is running!")
})

mux.Handle("/", awsAuth(handler))

}

func clientExample() (string, string, string, string) {

ctx := context.Background()

signatureClient, err := client.NewClient(ctx)
if err != nil {
panic(err)
}

xAmzDate, authorization, xAmzSecurityToken, awsRegion, err := signatureClient.GetStsParameters(ctx)
if err != nil {
panic(err)
}

return xAmzDate, authorization, xAmzSecurityToken, awsRegion

}

func serverExample(date, authorization, token, region string) {

identity, err := server.StsGetCallerIdentity("https://sts."+region+".amazonaws.com", date, authorization, token)
if err != nil {
panic(err)
}

fmt.Println("AWS IAM ARN = ", identity.GetCallerIdentityResult.Arn)

}
21 changes: 21 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module github.com/klemenkozelj/go-aws-http-authenticator

go 1.20

require (
github.com/aws/aws-sdk-go-v2 v1.19.0
github.com/aws/aws-sdk-go-v2/config v1.18.29
github.com/aws/aws-sdk-go-v2/credentials v1.13.28
github.com/aws/aws-sdk-go-v2/service/sts v1.20.0
)

require (
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
)
33 changes: 33 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
github.com/aws/aws-sdk-go-v2 v1.19.0 h1:klAT+y3pGFBU/qVf1uzwttpBbiuozJYWzNLHioyDJ+k=
github.com/aws/aws-sdk-go-v2 v1.19.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/config v1.18.29 h1:yA+bSSRGhBwWuprG9I4VgxfK//NBLZ/0BGOHiV3f9oM=
github.com/aws/aws-sdk-go-v2/config v1.18.29/go.mod h1:bJT6P8A+KU1qvNMp8aj+/NmaI06Z670dHNoWsrLOgMg=
github.com/aws/aws-sdk-go-v2/credentials v1.13.28 h1:WM9tEHgoOh5ThJZ042UKnSx7TXGSC/bz63X3fsrQL2o=
github.com/aws/aws-sdk-go-v2/credentials v1.13.28/go.mod h1:86BSbSeamnVVdr1hPfBZVN8SXM7KxSAZAvhNxVfi8fU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 h1:kP3Me6Fy3vdi+9uHd7YLr6ewPxRL+PU6y15urfTaamU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5/go.mod h1:Gj7tm95r+QsDoN2Fhuz/3npQvcZbkEf5mL70n3Xfluc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 h1:hMUCiE3Zi5AHrRNGf5j985u0WyqI6r2NULhUfo0N/No=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35/go.mod h1:ipR5PvpSPqIqL5Mi82BxLnfMkHVbmco8kUwO2xrCi0M=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 h1:yOpYx+FTBdpk/g+sBU6Cb1H0U/TLEcYYp66mYqsPpcc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29/go.mod h1:M/eUABlDbw2uVrdAn+UsI6M727qp2fxkp8K0ejcBDUY=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 h1:8r5m1BoAWkn0TDC34lUculryf7nUF25EgIMdjvGCkgo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36/go.mod h1:Rmw2M1hMVTwiUhjwMoIBFWFJMhvJbct06sSidxInkhY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 h1:IiDolu/eLmuB18DRZibj77n1hHQT7z12jnGO7Ze3pLc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 h1:sWDv7cMITPcZ21QdreULwxOOAmE05JjEsT6fCDtDA9k=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13/go.mod h1:DfX0sWuT46KpcqbMhJ9QWtxAIP1VozkDWf8VAkByjYY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 h1:BFubHS/xN5bjl818QaroN6mQdjneYQ+AOx44KNXlyH4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13/go.mod h1:BzqsVVFduubEmzrVtUFQQIQdFqvUItF8XUq2EnS8Wog=
github.com/aws/aws-sdk-go-v2/service/sts v1.20.0 h1:jKmIOO+dFvCPuIhhM8u0Dy3dtd590n2kEDSYiGHoI98=
github.com/aws/aws-sdk-go-v2/service/sts v1.20.0/go.mod h1:yVGZA1CPkmUhBdA039jXNJJG7/6t+G+EBWmFq23xqnY=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
20 changes: 20 additions & 0 deletions server/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package server

type ErrorType int

const (
AwsStsInvalidParameter ErrorType = iota
AwsStsRequestError
AwsStsServerError
AWSStsServerRejection
AWSStsServerResponse
)

type errorCustom struct {
Type ErrorType
Err error
}

func (se errorCustom) Error() string {
return se.Err.Error()
}
Loading

0 comments on commit bd82242

Please sign in to comment.