Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Smithy httpBearerAuth authentication trait #362

Merged
merged 6 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions auth/bearer/docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package bearer provides middleware and utilities for authenticating API
// operation calls with a Bearer Token.
package bearer
104 changes: 104 additions & 0 deletions auth/bearer/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package bearer

import (
"context"
"fmt"

"github.com/aws/smithy-go/middleware"
smithyhttp "github.com/aws/smithy-go/transport/http"
)

// Message is the middleware stack's request transport message value.
type Message interface{}

// Signer provides an interface for implementations to decorate a request
// message with a bearer token. The signer is responsible for validating the
// message type is compatible with the signer.
type Signer interface {
SignWithBearerToken(context.Context, Token, Message) (Message, error)
}

// AuthenticationMiddleware provides the Finalize middleware step for signing
// an request message with a bearer token.
type AuthenticationMiddleware struct {
signer Signer
tokenProvider TokenProvider
}

// AddAuthenticationMiddleware helper adds the AuthenticationMiddleware to the
// middleware Stack in the Finalize step with the options provided.
func AddAuthenticationMiddleware(s *middleware.Stack, signer Signer, tokenProvider TokenProvider) error {
return s.Finalize.Add(
NewAuthenticationMiddleware(signer, tokenProvider),
middleware.After,
)
}

// NewAuthenticationMiddleware returns an initialized AuthenticationMiddleware.
func NewAuthenticationMiddleware(signer Signer, tokenProvider TokenProvider) *AuthenticationMiddleware {
return &AuthenticationMiddleware{
signer: signer,
tokenProvider: tokenProvider,
}
}

const authenticationMiddlewareID = "BearerTokenAuthentication"

// ID returns the resolver identifier
func (m *AuthenticationMiddleware) ID() string {
return authenticationMiddlewareID
}

// HandleFinalize implements the FinalizeMiddleware interface in order to
// update the request with bearer token authentication.
func (m *AuthenticationMiddleware) HandleFinalize(
ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
) (
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
) {
token, err := m.tokenProvider.RetrieveBearerToken(ctx)
if err != nil {
return out, metadata, fmt.Errorf("failed AuthenticationMiddleware wrap message, %w", err)
}

signedMessage, err := m.signer.SignWithBearerToken(ctx, token, in.Request)
if err != nil {
return out, metadata, fmt.Errorf("failed AuthenticationMiddleware sign message, %w", err)
}

in.Request = signedMessage
return next.HandleFinalize(ctx, in)
}

// SignHTTPSMessage provides a bearer token authentication implementation that
// will sign the message with the provided bearer token.
//
// Will fail if the message is not a smithy-go HTTP request or the request is
// not HTTPS.
type SignHTTPSMessage struct{}

// NewSignHTTPSMessage returns an initialized signer for HTTP messages.
func NewSignHTTPSMessage() *SignHTTPSMessage {
return &SignHTTPSMessage{}
}

// SignWithBearerToken returns a copy of the HTTP request with the bearer token
// added via the "Authorization" header, per RFC 6750, https://datatracker.ietf.org/doc/html/rfc6750.
//
// Returns an error if the request's URL scheme is not HTTPS, or the request
// message is not an smithy-go HTTP Request pointer type.
func (SignHTTPSMessage) SignWithBearerToken(ctx context.Context, token Token, message Message) (Message, error) {
req, ok := message.(*smithyhttp.Request)
if !ok {
return nil, fmt.Errorf("expect smithy-go HTTP Request, got %T", message)
}

if !req.IsHTTPS() {
return nil, fmt.Errorf("bearer token with HTTP request requires HTTPS")
}

reqClone := req.Clone()
reqClone.Header.Set("Authorization", "Bearer "+token.Value)

return reqClone, nil
}
78 changes: 78 additions & 0 deletions auth/bearer/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package bearer

import (
"context"
"net/http"
"net/url"
"strings"
"testing"

smithyhttp "github.com/aws/smithy-go/transport/http"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)

func TestSignHTTPSMessage(t *testing.T) {
cases := map[string]struct {
message Message
token Token
expectMessage Message
expectErr string
}{
// Cases
"not smithyhttp.Request": {
message: struct{}{},
expectErr: "expect smithy-go HTTP Request",
},
"not https": {
message: func() Message {
r := smithyhttp.NewStackRequest().(*smithyhttp.Request)
r.URL, _ = url.Parse("http://example.aws")
return r
}(),
expectErr: "requires HTTPS",
},
"success": {
message: func() Message {
r := smithyhttp.NewStackRequest().(*smithyhttp.Request)
r.URL, _ = url.Parse("https://example.aws")
return r
}(),
token: Token{Value: "abc123"},
expectMessage: func() Message {
r := smithyhttp.NewStackRequest().(*smithyhttp.Request)
r.URL, _ = url.Parse("https://example.aws")
r.Header.Set("Authorization", "Bearer abc123")
return r
}(),
},
}

for name, c := range cases {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
signer := SignHTTPSMessage{}
message, err := signer.SignWithBearerToken(ctx, c.token, c.message)
if c.expectErr != "" {
if err == nil {
t.Fatalf("expect error, got none")
}
if e, a := c.expectErr, err.Error(); !strings.Contains(a, e) {
t.Fatalf("expect %v in error %v", e, a)
}
return
} else if err != nil {
t.Fatalf("expect no error, got %v", err)
}

options := []cmp.Option{
cmpopts.IgnoreUnexported(smithyhttp.Request{}),
cmpopts.IgnoreUnexported(http.Request{}),
}

if diff := cmp.Diff(c.expectMessage, message, options...); diff != "" {
t.Errorf("expect match\n%s", diff)
}
})
}
}
50 changes: 50 additions & 0 deletions auth/bearer/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package bearer

import (
"context"
"time"
)

// Token provides a type wrapping a bearer token and expiration metadata.
type Token struct {
Value string

CanExpire bool
Expires time.Time
}

// Expired returns if the token's Expires time is before or equal to the time
// provided. If CanExpires is false, Expired will always return false.
func (t Token) Expired(now time.Time) bool {
if !t.CanExpire {
return false
}
now = now.Round(0)
return now.Equal(t.Expires) || now.After(t.Expires)
}

// TokenProvider provides interface for retrieving bearer tokens.
type TokenProvider interface {
RetrieveBearerToken(context.Context) (Token, error)
}

// TokenProviderFunc provides a helper utility to wrap a function as a type
// that implements the TokenProvider interface.
type TokenProviderFunc func(context.Context) (Token, error)

// RetrieveBearerToken calls the wrapped function, returning the Token or
// error.
func (fn TokenProviderFunc) RetrieveBearerToken(ctx context.Context) (Token, error) {
return fn(ctx)
}

// StaticTokenProvider provides a utility for wrapping a static bearer token
// value within an implementation of a token provider.
type StaticTokenProvider struct {
Token Token
}

// RetrieveBearerToken returns the static token specified.
func (s StaticTokenProvider) RetrieveBearerToken(context.Context) (Token, error) {
return s.Token, nil
}