Created
February 4, 2024 23:36
-
-
Save JanisErdmanis/967325e2151a4d33f8e39ad30cb557d8 to your computer and use it in GitHub Desktop.
Integration API exploration for HMAC authorization
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Base64 | |
using Nettle | |
using Dates | |
sha256(data) = Nettle.digest("sha256", data) | |
sha256(data, key) = Nettle.digest("sha256", key, data) | |
function get_header(headers, key) | |
for (keyi, value) in headers | |
if keyi == key | |
return value | |
end | |
end | |
return nothing | |
end | |
verifyRequest(request, secret::String) = verifyRequest(request, base64decode(secret)) | |
function verifyRequest(request, secret::Vector{UInt8}) | |
(; method, target, headers, body) = request | |
# Compute content hash | |
contentHash = Base64.base64encode(sha256(body)) | |
# Extract received content hash and signature | |
contentHashReceived = get_header(headers, "x-ms-content-sha256") | |
authorizationHeader = get_header(headers, "Authorization") | |
signatureReceived = split(authorizationHeader, "&Signature=")[end] | |
# Verify content hash | |
if contentHash != contentHashReceived | |
return false | |
end | |
# Recreate string to sign | |
utcNow = get_header(headers, "x-ms-date") | |
host = get_header(headers, "Host") | |
signedHeaders = "x-ms-date;host;x-ms-content-sha256" | |
stringToSign = join([method, target, join([utcNow, host, contentHashReceived], ";")], '\n') | |
# Recreate the HMAC signature | |
hmac = sha256(stringToSign, secret) | |
signature = base64encode(hmac) | |
# Verify the signature | |
return signature == signatureReceived | |
end | |
signRequest(host, method, url, body, credential, secret::String) = signRequest(host, method, url, body, credential, base64decode(secret)) | |
function signRequest(host, method, url, body, credential, secret::Vector{UInt8}) | |
contentHash = Base64.base64encode(sha256(body)) | |
utcNow = Dates.format(Dates.now(), "E, dd u yyyy HH:MM:SS") * " GMT" | |
signedHeaders = "x-ms-date;host;x-ms-content-sha256" | |
stringToSign = join([method, url, join([utcNow, host, contentHash], ";")], '\n') | |
hmac = sha256(stringToSign, secret) | |
signature = base64encode(hmac) | |
return [ | |
"x-ms-date" => utcNow, | |
"x-ms-content-sha256" => contentHash, | |
"Authorization" => "HMAC-SHA256 Credential=" * credential * "&SignedHeaders=" * signedHeaders * "&Signature=" * signature | |
] | |
end | |
function timestamp(date_str) | |
date_format = DateFormat("dd u yyyy HH:MM:SS") | |
relevant_part = join(split(date_str)[2:end-1], " ") | |
parsed_date = DateTime(relevant_part, date_format) | |
return parsed_date | |
end | |
function credential(authorization_string) | |
regex = r"Credential=([^&]+)" | |
m = match(regex, authorization_string) | |
credential = m !== nothing ? m.captures[1] : "No match found" | |
return credential | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function signRequest(host, | |
method, // GET, PUT, POST, DELETE | |
url, // path+query | |
body, // request body (undefined of none) | |
credential, // access key id | |
secret) // access key value (base64 encoded) | |
{ | |
var verb = method.toUpperCase(); | |
var utcNow = new Date().toUTCString(); // TODO: exact time needs to be obfuscated for anonymity | |
var contentHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Base64); | |
// | |
// SignedHeaders | |
var signedHeaders = "x-ms-date;host;x-ms-content-sha256"; // Semicolon separated header names | |
// | |
// String-To-Sign | |
var stringToSign = | |
verb + '\n' + // VERB | |
url + '\n' + // path_and_query | |
utcNow + ';' + host + ';' + contentHash; // Semicolon separated SignedHeaders values | |
// Signature | |
var signature = CryptoJS.HmacSHA256(stringToSign, CryptoJS.enc.Base64.parse(secret)).toString(CryptoJS.enc.Base64); | |
// | |
// Result request headers | |
return [ | |
{ name: "x-ms-date", value: utcNow }, | |
{ name: "x-ms-content-sha256", value: contentHash }, | |
{ name: "Authorization", value: "HMAC-SHA256 Credential=" + credential + "&SignedHeaders=" + signedHeaders + "&Signature=" + signature } | |
]; | |
} | |
function verifyRequest(request, secretBase64) { | |
const { method, url, headers, body } = request; | |
// Extract the necessary headers | |
const utcNow = headers['x-ms-date']; | |
const contentHashReceived = headers['x-ms-content-sha256']; | |
const authorizationHeader = headers['authorization'] ?? headers['Authorization']; | |
const host = headers['host'] ?? headers['Host']; | |
// Recreate the content hash for verification | |
const contentHash = body ? CryptoJS.SHA256(body).toString(CryptoJS.enc.Base64) : CryptoJS.enc.Base64.stringify(CryptoJS.SHA256('')); | |
// Verify content hash | |
if (contentHash !== contentHashReceived) { | |
return false; // Hash mismatch indicates the body was altered | |
} | |
// Extract signature from the Authorization header | |
// Assuming the Authorization header format is "HMAC-SHA256 Credential=credential&SignedHeaders=signedHeaders&Signature=signature" | |
const signatureReceived = authorizationHeader.split('&Signature=')[1]; | |
if (!signatureReceived) { | |
return false; // Signature not found in the Authorization header | |
} | |
// Recreate string to sign | |
const signedHeaders = "x-ms-date;host;x-ms-content-sha256"; | |
const stringToSign = method.toUpperCase() + '\n' + url + '\n' + utcNow + ';' + host + ';' + contentHash; | |
// Decode the base64 secret for signing | |
const secret = CryptoJS.enc.Base64.parse(secretBase64); | |
// Recreate the signature using the secret | |
const signature = CryptoJS.HmacSHA256(stringToSign, secret).toString(CryptoJS.enc.Base64); | |
// Verify the recreated signature against the received signature | |
return signature === signatureReceived; | |
} | |
function fetchAuthorized(action, request, key) { | |
const {method, headers, body} = request; | |
// Parse the URL | |
const url = new URL(action); | |
const host = url.host; | |
const path = url.pathname; | |
// Deriving credential as a hash from the key | |
let credential = CryptoJS.SHA256(key).toString(CryptoJS.enc.Base64); | |
let authHeaders = signRequest(host, method, path, body, credential, key); | |
headers['Host'] = host; | |
return fetch(action, { | |
method: method, | |
headers: authHeaders.reduce((acc, header) => { | |
acc[header.name] = header.value; | |
return acc; | |
}, headers), | |
body: body | |
}).then(response => { | |
if (response.status === 401) { | |
return response.text().then(message => { | |
// Construct an error message including the server's response | |
const errorMessage = message || "Unauthorized: Access is denied due to invalid credentials."; | |
throw new Error(errorMessage); | |
}); | |
} | |
// Make a copy of the response to read the body without consuming it | |
const clonedResponse = response.clone(); | |
// Now, use the cloned response for authorization check | |
return clonedResponse.text().then(body => { | |
let responseHeaders = {}; | |
// Iterate over response headers and collect them | |
response.headers.forEach((value, name) => { | |
responseHeaders[name] = value; | |
}); | |
responseHeaders['host'] = host; | |
if (verifyRequest({ | |
method: "REPLY", | |
url: path, | |
body: body, | |
headers: responseHeaders | |
}, key)) { | |
console.log("Response Authorized"); | |
// Return the original response object | |
return response; | |
} else { | |
// Throw an error or handle unauthorized response | |
throw new Error("Response Unauthorized or Verification Failed"); | |
} | |
}); | |
}).catch(error => { | |
console.error("Error in fetch_authorized:", error); | |
throw error; // Re-throw or handle as needed | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Centered Card Form with Equal Widths</title> | |
<style> | |
body, html { | |
height: 100%; | |
margin: 0; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
background-color: #f5f5f5; | |
} | |
.card { | |
display: flex; | |
box-shadow: 0 8px 16px rgba(0,0,0,0.1); | |
background-color: white; | |
border-radius: 10px; | |
} | |
.form-container, .output { | |
width: 250px; /* Fixed width for both sides */ | |
padding: 20px; | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
border-top-right-radius: 10px; | |
border-bottom-right-radius: 10px; | |
word-wrap: break-word; | |
} | |
.output { | |
background-color: #f9f9f9; | |
} | |
input[type="text"], button { | |
width: calc(100% - 20px); /* Adjust width to account for padding */ | |
padding: 10px; | |
margin-bottom: 10px; | |
font-size: 16px; | |
} | |
input { | |
border-style: solid; | |
border-width: 1px; | |
border-color: #c9c9c9; | |
} | |
button { | |
cursor: pointer; | |
width: 100px; | |
} | |
</style> | |
<script src="node_modules/crypto-js/crypto-js.js"></script> | |
<script src="auth.js"></script> | |
</head> | |
<body> | |
<div class="card"> | |
<div class="form-container"> | |
<form id="myForm" action="http://127.0.0.1:8546/test" method="POST"> | |
<input type="text" name="message" placeholder="Message"> | |
<input type="text" name="key" placeholder="Key"> | |
<button type="button" onclick="submitForm()">Test</button> | |
</form> | |
</div> | |
<div id="output" class="output"></div> | |
</div> | |
<script> | |
function submitForm() { | |
// Prevent the form from submitting traditionally | |
event.preventDefault(); | |
// Get the name and surname values | |
let message = document.getElementsByName('message')[0].value; | |
let key = document.getElementsByName('key')[0].value; | |
form = document.getElementById('myForm') | |
fetchAuthorized(form.action, { | |
method: form.method, | |
headers: {'Content-Type': "text"}, | |
body: message}, key) | |
.then(response => { | |
// Extract the text (body) from the response object | |
return response.text(); // This returns a Promise that resolves with the text body | |
}) | |
.then(text => { | |
// Now 'text' contains the actual body of the response | |
document.getElementById('output').textContent = text; | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
document.getElementById('output').textContent = error; | |
}); | |
} | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Oxygen | |
using HTTP: Request, Response, method | |
using Base64 | |
include("azure.jl") | |
timestamp(req::Request) = timestamp(get_header(req.headers, "x-ms-date")) | |
credential(req::Request) = credential(get_header(req.headers, "Authorization")) | |
# Authorization Middleware | |
AUTHORIZATION_STORE = Dict{String, String}() | |
authorize(secret) = AUTHORIZATION_STORE[base64encode(sha256(secret))] = secret | |
retrieve_secret(cred) = AUTHORIZATION_STORE[cred] | |
authorize("secret") | |
function AuthorizationMiddleware(handler) | |
return function(req::Request) | |
println("Authorization middleware") | |
local cred, secret | |
try | |
cred = credential(req) | |
secret = retrieve_secret(cred) | |
catch | |
return Response(401, "Invalid Credential") | |
end | |
# getting a coresponding secret here | |
if verifyRequest(req, secret) | |
response = handler(req) | |
host = get_header(req.headers, "Host") | |
auth_headers = signRequest(host, "REPLY", req.target, response.body, cred, secret) | |
append!(response.headers, auth_headers) | |
return response | |
else | |
# The error could be more granular | |
# that would seemingly require to throw an error | |
# in verify request and forward it as a response | |
return Response(401, "Unauthorized Access") | |
end | |
end | |
end | |
# CORS Middleware | |
const CORS_HEADERS = [ | |
"Access-Control-Allow-Origin" => "*", | |
"Access-Control-Allow-Headers" => "*, Authorization", | |
"Access-Control-Allow-Methods" => "POST, GET, OPTIONS", | |
"Access-Control-Expose-Headers" => "x-ms-date, x-ms-content-sha256, Authorization" | |
] | |
function CorsMiddleware(handler) | |
return function(req::Request) | |
println("CORS middleware") | |
# determine if this is a pre-flight request from the browser | |
if method(req)=="OPTIONS" | |
return Response(200, CORS_HEADERS) | |
else | |
response = handler(req) # passes the request to the AuthMiddleware | |
append!(response.headers, CORS_HEADERS) | |
return response | |
end | |
end | |
end | |
@post "/test" function(req::Request) | |
@show timestamp(req) | |
@show credential(req) | |
@show verifyRequest(req, "secret") | |
body = "Server Confirms Receiving: $(String(req.body))" | |
return Response(200, body) | |
end | |
Oxygen.serve(port=8546, middleware=[CorsMiddleware, AuthorizationMiddleware]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using JSON3 | |
include("auth.jl") | |
secret = "mySecretKey" | |
# Testing compatability with JS implementation | |
request = read("requestObject.json") |> JSON3.read | |
(; method, url, body) = request | |
headers = [string(key) => value for (key, value) in request.headers] | |
verifyRequest((; method, target=url, headers, body), secret) | |
# Testing signRequest method | |
host = "peacefounder.org" | |
method = "POST" | |
target = "/register" | |
body = "Hello World!" | |
credential_str = "exampleCredential" | |
headers = signRequest(host, method, target, body, credential_str, secret) | |
push!(headers, "Host" => host) | |
verifyRequest((; method, target, headers, body), secret) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const CryptoJS = require("crypto-js"); | |
// For simplicity evaling the file in the scope as it is done in browser | |
const fs = require('fs'); | |
const path = require('path'); | |
eval(fs.readFileSync(path.join(__dirname, "auth.js"), 'utf8')); | |
// Example call to signRequest | |
let host = "peacefounder.org"; | |
let method = "POST"; | |
let url = "/test"; | |
let body = "Hello World!" | |
let credential = "exampleCredential"; | |
let secret = "mySecretKey"; // base64 form | |
let authHeaders = signRequest(host, method, url, body, credential, secret); | |
let headersObject = authHeaders.reduce((acc, header) => { | |
acc[header.name] = header.value; | |
return acc; | |
}, {'Host': host}); | |
let requestObject = { | |
method: method, | |
url: url, | |
headers: headersObject, | |
body: body | |
} | |
//console.log(requestObject); | |
console.log(verifyRequest(requestObject, secret)); | |
// Storing the request to a file | |
const jsonString = JSON.stringify(requestObject, null, 2); // Beautify the JSON string | |
const filePath = path.join(__dirname, 'requestObject.json'); | |
try { | |
fs.writeFileSync(filePath, jsonString); | |
console.log('File has been written'); | |
} catch (error) { | |
console.error('An error occurred:', error); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment