Skip to content

Instantly share code, notes, and snippets.

@JanisErdmanis
Created February 4, 2024 23:36
Show Gist options
  • Save JanisErdmanis/967325e2151a4d33f8e39ad30cb557d8 to your computer and use it in GitHub Desktop.
Save JanisErdmanis/967325e2151a4d33f8e39ad30cb557d8 to your computer and use it in GitHub Desktop.
Integration API exploration for HMAC authorization
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
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
});
}
<!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>
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])
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)
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