Skip to content

Commit

Permalink
Implementation of HMAC authorizer (#20)
Browse files Browse the repository at this point in the history
* Implementation of HMAC authorizer

* Add AWSv4 signature authorizer

* Bugfixes; auth_handler error processing

* Improve error handling

* Small fixes
  • Loading branch information
thheinen authored Sep 6, 2022
1 parent 6a6e35a commit 33d23e6
Show file tree
Hide file tree
Showing 13 changed files with 318 additions and 15 deletions.
18 changes: 18 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ARG VARIANT="3.0"
FROM mcr.microsoft.com/vscode/devcontainers/ruby:0-${VARIANT}

ARG DIRENV_VERSION=2.32.1

RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends jq vim direnv yamllint python3-pip python3-setuptools git less python3-dev

COPY requirements.txt /tmp/
RUN pip3 install --requirement /tmp/requirements.txt

RUN curl --location-trusted https://github.com/direnv/direnv/releases/download/v${DIRENV_VERSION}/direnv.linux-amd64 --output /usr/local/bin/direnv --silent \
&& chmod +x /usr/local/bin/direnv

RUN echo -e "setlocal noautoindent\nsetlocal nocindent\nsetlocal nosmartindent\nsetlocal indentexpr=\"\n" > /home/${USER}/.vimrc \
&& echo 'alias awsume="source awsume"' >> /home/${USER}/.bashrc \
&& echo 'eval "$(direnv hook bash)"' >> /home/${USER}/.bashrc \
&& mkdir /home/${USER}/.aws
32 changes: 32 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "Ruby",
"build": {
"dockerfile": "Dockerfile",
"args": {
"VARIANT": "3",
"INSTALL_NODE": "false"
}
},
"mounts": [
"source=${localEnv:HOME}/.aws,target=/home/vscode/.aws,readonly,type=bind",
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,readonly,type=bind",
"target=/home/vscode/.aws/cli/cache,type=tmpfs,tmpfs-mode=1777"
],
"extensions": [
"editorconfig.editorconfig",
"rebornix.ruby",
"redhat.vscode-yaml",
"VisualStudioExptTeam.vscodeintellicode",
"wingrunr21.vscode-ruby"
],
"remoteUser": "vscode",
"remoteEnv": {
"PATH": "/home/vscode/bin:/home/vscode/.local/bin:${containerEnv:PATH}",

"AWS_ACCESS_KEY_ID": "${localEnv:AWS_ACCESS_KEY_ID}",
"AWS_SECRET_ACCESS_KEY": "${localEnv:AWS_SECRET_ACCESS_KEY}",
"AWS_REGION": "${localEnv:AWS_REGION}",
"AWS_SESSION_TOKEN": "${localEnv:AWS_SESSION_TOKEN}"
},
"postAttachCommand": "bundle config set --local with 'development' && awsume-configure && direnv allow ${containerWorkspaceFolder} || true"
}
2 changes: 2 additions & 0 deletions .devcontainer/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
awscli
awsume
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## Version 0.5.0

- Add proper processing of 400/401 errors
- Add auth_handler specific error processing
- Add new AWS v4 signature authorizer
- Add new HMAC signature authorizer (#19)
- Add VSCode Dev Container
- Fix merging of user-provided and auth_handler-derived headers
- Fix missing prefix accessing `AuthHandler`
- Fix naming of auth_handlers which involve abbreviations

## Version 0.4.2

- Add Apache2 license file
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Provides a transport to communicate easily with RESTful APIs.
## Requirements

- Gem `rest-client` in Version 2.1
- Gem `awssig-v4`

## Installation

Expand Down Expand Up @@ -44,6 +45,22 @@ Identifier: `auth_type: :authtype_apikey`
| -------------------- | --------------------------------------- | ----------- |
| `apikey` | API Key for authentication | _required_ |

### AWS Signature v4

Identifier: `auth_type: :awsv4`

| Option | Explanation | Default |
| ------------------- | ----------------------------- | ------------------------ |
| `credentials` | Type of credentials to use | `access_keys` |
| `access_key` | ID of the access key | ENV: `ACCESS_KEY_ID` |
| `secret_access_key` | Secret part of the access key | ENV: `SECRET_ACCESS_KEY` |

Only `access_keys` are supported as a credential currently. Support for other types,
like EC2 roles, is planned.

Access key and secret access key are pulled from the mentioned environment variables,
if they are not provided.

### Basic (RFC 2617)

Identifier: `auth_type: :basic`
Expand Down Expand Up @@ -79,6 +96,19 @@ Identifier: `auth_type: :header`
| `apikey` | API Key for authentication | _required_ |
| `header` | Name of the HTTP header to include | `X-API-Key` |

### HMAC Signature

Identifier: `auth_type: :hmac_signature`

| Option | Explanation | Default |
| -------------------- | --------------------------------------- | ------------- |
| `hmac_secret` | Shared secret to use for signing on | _required_ |
| `header` | Name of header to add | `X-Signature` |
| `digest` | OpenSSL Digest type supported by Ruby | `SHA256` |

This will use the request body (payload) and sign it using HMAC. For a full list of
supported digest, look at [the Ruby documentation](https://ruby-doc.org/stdlib-2.7.0/libdoc/openssl/rdoc/OpenSSL/Digest.html)

### Redfish

Identifier: `auth_type: :redfish`
Expand Down
6 changes: 5 additions & 1 deletion lib/train-rest.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
libdir = File.dirname(__FILE__)
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)

require "train-rest/errors"
require "train-rest/version"

require "train-rest/transport"
require "train-rest/connection"

require "train-rest/auth_handler"
require "train-rest/auth_handler/awsv4"
require "train-rest/auth_handler/anonymous"
require "train-rest/auth_handler/authtype-apikey"
require "train-rest/auth_handler/header"
require "train-rest/auth_handler/basic"
require "train-rest/auth_handler/bearer"
require "train-rest/auth_handler/header"
require "train-rest/auth_handler/hmac-signature"
require "train-rest/auth_handler/redfish"
27 changes: 26 additions & 1 deletion lib/train-rest/auth_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ def auth_parameters
{ headers: auth_headers }
end

# This Auth Handler will need payload, URI and headers, e.g. for signatures.
#
# @return [Boolean]
def signature_based?
false
end

# Return headers based on payload signing.
#
# @param [Hash] data different types of data for processing
# @option data [String] :payload contents of the message body
# @option data [Hash] :headers existing headers to the request
# @option data [String] :url URL which will be requested
# @option data [Symbol] :method Method to execute
# @returns [Hash]
def process(payload: "", headers: {}, url: "", method: nil)
{}
end

# Allow processing errors related to authentication.
#
# @param [RestClient::Exception] error raw error data
def process_error(_error); end

class << self
private

Expand All @@ -77,7 +101,8 @@ class << self
# @see https://github.com/chef/chef/blob/main/lib/chef/mixin/convert_to_class_name.rb
def convert_to_snake_case(str)
str = str.dup
str.gsub!(/[A-Z]/) { |s| "_" + s }
str.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
str.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
str.downcase!
str.sub!(/^\_/, "")
str
Expand Down
119 changes: 119 additions & 0 deletions lib/train-rest/auth_handler/awsv4.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
require 'aws-sigv4'
require 'json'

require_relative "../auth_handler"

module TrainPlugins
module Rest
class AWSV4 < AuthHandler
VALID_CREDENTIALS = %w[
access_keys
].freeze

SIGNED_HEADERS = %w[
content-type host x-amz-date x-amz-target
].freeze

def check_options
options[:credentials] ||= "access_keys"

unless VALID_CREDENTIALS.include? credentials
raise ArgumentError.new("Invalid type of credentials: #{credentials}")
end

if access_keys?
raise ArgumentError.new('Missing `access_key` credential') unless access_key
raise ArgumentError.new('Missing `secret_access_key` credential') unless secret_access_key
end
end

def signature_based?
true
end

def process(payload: "", headers: {}, url: "", method: nil)
headers.merge! ({
'Accept-Encoding' => 'identity',
'User-Agent' => "train-rest/#{TrainPlugins::Rest::VERSION}",
'Content-Type' => 'application/x-amz-json-1.0'
})

signed_headers = headers.select do |name, _value|
SIGNED_HEADERS.include? name.downcase
end

@url = url

signature = signer(url).sign_request(
http_method: method.to_s.upcase,
url: url,
headers: signed_headers,
body: payload.to_json
)

{
headers: headers.merge(signature.headers)
}
end

def process_error(error)
raise AuthenticationError.new("Authentication failed: #{error.response.to_s.chop}") if error.response.code == 401
raise BadRequest.new("Bad request: #{error.response.to_s.chop}") if error.response.code == 400

message = JSON.parse(error.response.to_s)

raise AuthenticationError.new(message["message"] || message["__type"])
rescue JSON::ParserError => e
raise AuthenticationError.new(error.response.to_s)
end

def access_key
options[:access_key] || ENV['AWS_ACCESS_KEY_ID']
end

def region(url = default_url)
url.delete_prefix('https://').split('.').at(1)
end

private

def credentials
options[:credentials]
end

def default_url
options[:endpoint]
end

def access_keys?
credentials == 'access_keys'
end

def secret_access_key
options[:secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY']
end

def service(url)
url.delete_prefix('https://').split('.').at(0)
end

def signer(url)
Aws::Sigv4::Signer.new(
service: service(url),
region: region(url),

**signer_credentials
)
end

def signer_credentials
if access_keys?
{
access_key_id: access_key,
secret_access_key: secret_access_key
}
end
end
end
end
end
39 changes: 39 additions & 0 deletions lib/train-rest/auth_handler/hmac-signature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require_relative "../auth_handler"

module TrainPlugins
module Rest
# Authentication via HMAC Signature.
class HmacSignature < AuthHandler
def check_options
raise ArgumentError.new("Need :hmac_secret for HMAC signatures") unless options[:hmac_secret]

options[:header] ||= "X-Signature"
options[:digest] ||= "SHA256"
end

def hmac_secret
options[:hmac_secret]
end

def digest
options[:digest]
end

def header
options[:header]
end

def signature_based?
true
end

def process(payload: "", headers: {}, url: "", method: nil)
{
headers: {
header => OpenSSL::HMAC.hexdigest(digest, hmac_secret, payload)
}
}
end
end
end
end
Loading

0 comments on commit 33d23e6

Please sign in to comment.