Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
GrantBirki committed Oct 13, 2024
1 parent 5f58ae6 commit 00ac75f
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 60 deletions.
58 changes: 58 additions & 0 deletions spec/kemal-hmac/kemal-hmac_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
require "../spec_helper"

describe "Kemal::Hmac" do
# it "goes to next handler with correct credentials" do
# hmac_handler = Kemal::Hmac::Handler.new()
# request = HTTP::Request.new(
# "GET",
# "/",
# headers: HTTP::Headers{"foo" => "bar"},
# )

# io, context = create_request_and_return_io_and_context(hmac_handler, request)
# response = HTTP::Client::Response.from_io(io, decompress: false)
# response.status_code.should eq 404
# context.kemal_authorized_client?.should eq("serdar")
# end

it "returns 401 when a header is provided but it is not for hmac auth" do
hmac_handler = Kemal::Hmac::Handler.new
request = HTTP::Request.new(
"GET",
"/",
headers: HTTP::Headers{"x-no-hmac-auth-whoops" => "foobar"},
)
io, context = create_request_and_return_io_and_context(hmac_handler, request)
response = HTTP::Client::Response.from_io(io, decompress: false)
response.status_code.should eq 401
response.headers["missing-hmac-headers"].should eq "HTTP_X_HMAC_CLIENT,HTTP_X_HMAC_TIMESTAMP,HTTP_X_HMAC_TOKEN"
context.kemal_authorized_client?.should eq(nil)
end

it "returns 401 when no headers are provided at all" do
hmac_handler = Kemal::Hmac::Handler.new
request = HTTP::Request.new(
"GET",
"/"
)
io, context = create_request_and_return_io_and_context(hmac_handler, request)
response = HTTP::Client::Response.from_io(io, decompress: false)
response.status_code.should eq 401
response.headers["missing-hmac-headers"].should eq "HTTP_X_HMAC_CLIENT,HTTP_X_HMAC_TIMESTAMP,HTTP_X_HMAC_TOKEN"
context.kemal_authorized_client?.should eq(nil)
end

it "returns 401 when only the client hmac header is provided" do
hmac_handler = Kemal::Hmac::Handler.new
request = HTTP::Request.new(
"GET",
"/",
headers: HTTP::Headers{"HTTP_X_HMAC_CLIENT" => "octo-client"},
)
io, context = create_request_and_return_io_and_context(hmac_handler, request)
response = HTTP::Client::Response.from_io(io, decompress: false)
response.status_code.should eq 401
response.headers["missing-hmac-headers"].should eq "HTTP_X_HMAC_TIMESTAMP,HTTP_X_HMAC_TOKEN"
context.kemal_authorized_client?.should eq(nil)
end
end
10 changes: 0 additions & 10 deletions spec/lib/crystal-base-template_spec.cr

This file was deleted.

20 changes: 20 additions & 0 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
require "spec"
require "../src/kemal-hmac"

class SpecAuthHandler < Kemal::Hmac::Handler
only ["/api"]

def call(context)
return call_next(context) unless only_match?(context)
super
end
end

def create_request_and_return_io_and_context(handler, request)
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
handler.call(context)
response.close
io.rewind
{io, context}
end
64 changes: 14 additions & 50 deletions src/kemal-hmac.cr
Original file line number Diff line number Diff line change
@@ -1,53 +1,17 @@
module Kemal::BasicAuth
require "base64"
require "kemal"
require "./kemal-hmac/**"

# This middleware adds HTTP Basic Auth support to your application.
# Returns 401 "Unauthorized" with wrong credentials.
#
# ```crystal
# basic_auth "username", "password"
# # basic_auth {"username1" => "password2", "username2" => "password2"}
# ```
#
# `HTTP::Server::Context#authorized_username` is set when the user is
# authorized.
class Handler < Kemal::Handler
BASIC = "Basic"
AUTH = "Authorization"
AUTH_MESSAGE = "Could not verify your access level for that URL.\nYou have to login with proper credentials"
HEADER_LOGIN_REQUIRED = "Basic realm=\"Login Required\""

def initialize(@credentials : Credentials)
end

# backward compatibility
def initialize(username : String, password : String)
initialize({ username => password })
end

def initialize(hash : Hash(String, String))
initialize(Credentials.new(hash))
end

def call(context)
if context.request.headers[AUTH]?
if value = context.request.headers[AUTH]
if value.size > 0 && value.starts_with?(BASIC)
if username = authorize?(value)
context.kemal_authorized_username = username
return call_next(context)
end
end
end
end
headers = HTTP::Headers.new
context.response.status_code = 401
context.response.headers["WWW-Authenticate"] = HEADER_LOGIN_REQUIRED
context.response.print AUTH_MESSAGE
end

def authorize?(value) : String?
username, password = Base64.decode_string(value[BASIC.size + 1..-1]).split(":")
@credentials.authorize?(username, password)
end
module Kemal
module Hmac
end
end

# Helper to easily add HTTP Basic Auth support.
def hmac_auth(hmac_client_header : String? = nil, hmac_timestamp_header : String? = nil, hmac_token_header : String? = nil)
add_handler Kemal.config.hmac_handler.new(
hmac_client_header: hmac_client_header,
hmac_timestamp_header: hmac_timestamp_header,
hmac_token_header: hmac_token_header
)
end
7 changes: 7 additions & 0 deletions src/kemal-hmac/ext/config.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "../handler"

module Kemal
class Config
property hmac_handler : Kemal::Hmac::Handler.class = Kemal::Hmac::Handler
end
end
3 changes: 3 additions & 0 deletions src/kemal-hmac/ext/context.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class HTTP::Server::Context
property? kemal_authorized_client : String?
end
62 changes: 62 additions & 0 deletions src/kemal-hmac/handler.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require "kemal"

module Kemal::Hmac
# This middleware adds hmac support to your application.
# Returns 401 "Unauthorized" with wrong credentials.
#
# ```
# hmac_auth "todo"
# # hmac_auth ["todo1", "todo2"]
# ```
#
# `HTTP::Server::Context#authorized_hmac_client` is set when the client is
# authorized.
class Handler < Kemal::Handler
HMAC_CLIENT_HEADER = ENV.fetch("HMAC_CLIENT_HEADER", "HTTP_X_HMAC_CLIENT")
HMAC_TIMESTAMP_HEADER = ENV.fetch("HMAC_TIMESTAMP_HEADER", "HTTP_X_HMAC_TIMESTAMP")
HMAC_TOKEN_HEADER = ENV.fetch("HMAC_TOKEN_HEADER", "HTTP_X_HMAC_TOKEN")

def initialize(hmac_client_header : String? = nil, hmac_timestamp_header : String? = nil, hmac_token_header : String? = nil)
@hmac_client_header = hmac_client_header || HMAC_CLIENT_HEADER
@hmac_timestamp_header = hmac_timestamp_header || HMAC_TIMESTAMP_HEADER
@hmac_token_header = hmac_token_header || HMAC_TOKEN_HEADER
@required_hmac_headers = [
@hmac_client_header,
@hmac_timestamp_header,
@hmac_token_header,
]
end

def call(context)
headers = load_hmac_headers(context)
missing_headers = missing_hmac_headers(headers)

# if any of the required headers are missing, return 401
unless missing_headers.empty?
context.response.status_code = 401
context.response.headers["missing-hmac-headers"] = missing_headers.join(",")
context.response.print "Unauthorized"
return
end

context.kemal_authorized_client = headers[@hmac_client_header]
end

# Load the required headers from the request for hmac authentication
def load_hmac_headers(context) : Hash(String, String?)
@required_hmac_headers.each_with_object({} of String => String?) do |name, hash|
hash[name] = context.request.headers.fetch(name, nil)
end
end

# If any of the required headers are missing, return the missing headers
def missing_hmac_headers(headers : Hash(String, String?)) : Array(String)
headers.select { |_, v| v.nil? }.keys
end

def authorize?(value) : String?
username, password = Base64.decode_string(value[BASIC.size + 1..-1]).split(":")
@credentials.authorize?(username, password)
end
end
end

0 comments on commit 00ac75f

Please sign in to comment.