mirror of
https://github.com/AskDavis/cyphernode.git
synced 2026-01-01 04:25:58 -08:00
Authenticated HTTP API - first pass
This commit is contained in:
19
api_auth_docker/Dockerfile
Normal file
19
api_auth_docker/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
git \
|
||||
openssl \
|
||||
fcgiwrap \
|
||||
spawn-fcgi \
|
||||
curl \
|
||||
jq
|
||||
|
||||
COPY auth.sh /etc/nginx/conf.d
|
||||
COPY default-ssl.conf /etc/nginx/conf.d/default.conf
|
||||
COPY entrypoint.sh entrypoint.sh
|
||||
COPY keys.properties /etc/nginx/conf.d
|
||||
COPY trace.sh /etc/nginx/conf.d
|
||||
|
||||
RUN chmod +x /etc/nginx/conf.d/auth.sh entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
120
api_auth_docker/README.md
Normal file
120
api_auth_docker/README.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# HTTP/S API supporting HMAC API keys
|
||||
|
||||
So all the other containers are in the Docker Swarm and we want to expose a real HTTP/S interface to clients outside of the Swarm, that makes sense. Clients have to get an API key first.
|
||||
|
||||
## API key
|
||||
|
||||
Let's produce a 256-bits key that we'll convert in an hex string to store and use with openssl hmac feature.
|
||||
|
||||
```shell
|
||||
dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32
|
||||
```
|
||||
|
||||
The key is stored in keys.properties and must be given to the client. This is a secret key. keys.properties looks like this:
|
||||
|
||||
```property
|
||||
#keyid=hex(key)
|
||||
key001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36
|
||||
key002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83
|
||||
```
|
||||
|
||||
### Bearer token
|
||||
|
||||
Following JWT (JSON Web Tokens) standard, we build a bearer token that will be in the request header and signed with the secret key. We need this in the request header:
|
||||
|
||||
```shell
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
...where token is:
|
||||
|
||||
```shell
|
||||
token = hhh.ppp.sss
|
||||
```
|
||||
|
||||
...where hhh is the header in base64, ppp is the payload in base64 and sss is the signature. Here are the expected formats and contents:
|
||||
|
||||
```shell
|
||||
header = {"alg":"HS256","typ":"JWT"}
|
||||
header64 = base64(header) = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==
|
||||
```
|
||||
|
||||
```shell
|
||||
payload = {"id":"001","exp":1538528077}
|
||||
payload64 = base64(payload) = eyJpZCI6IjAwMSIsImV4cCI6MTUzODUyODA3N30K
|
||||
```
|
||||
|
||||
The "id" property is the client id and the "exp" property should be current epoch time + 10 seconds, like:
|
||||
|
||||
```shell
|
||||
$((`date +"%s"`+10))
|
||||
```
|
||||
|
||||
...so that the request will be expired in 10 seconds. That should take care of most Replay attacks if any. You should run nginx with TLS so that the replay attack can't be possible.
|
||||
|
||||
```shell
|
||||
signature = hmacsha256(header64.payload64, key)
|
||||
```
|
||||
|
||||
```shell
|
||||
token = header64 + "." + payload64 + "." + signature
|
||||
```
|
||||
|
||||
### cURL example of an API invocation
|
||||
|
||||
Instruction should be in the form:
|
||||
|
||||
```shell
|
||||
curl -v -H "Authorization: Bearer hhh.ppp.sss" localhost
|
||||
```
|
||||
|
||||
10 seconds request expiration:
|
||||
|
||||
```shell
|
||||
id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash
|
||||
```
|
||||
|
||||
60 seconds request expiration:
|
||||
|
||||
```shell
|
||||
id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash
|
||||
```
|
||||
|
||||
## SSL
|
||||
|
||||
Create your key and certificates.
|
||||
|
||||
openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ~/cyphernode/private/key.pem -out ~/cyphernode/certs/cert.pem -days 365
|
||||
|
||||
Use default-ssl.conf as the template instead of default.conf.
|
||||
|
||||
## Build
|
||||
|
||||
### Create your API key and put it in keys.properties
|
||||
|
||||
```shell
|
||||
dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32
|
||||
```
|
||||
|
||||
### Build and run docker image
|
||||
|
||||
```shell
|
||||
docker build -t authapi .
|
||||
docker run -d --rm --name authapi -p 80:80 -p 443:443 --network cyphernodenet -v "~/cyphernode/certs:/etc/ssl/certs" -v "~/cyphernode/private:/etc/ssl/private" authapi
|
||||
```
|
||||
|
||||
## Invoke cyphernode through authenticated API
|
||||
|
||||
```shell
|
||||
id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash
|
||||
```
|
||||
|
||||
## Technicalities
|
||||
|
||||
```shell
|
||||
h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64)
|
||||
p64=$(echo "{\"id\":\"001\",\"exp\":$((`date +"%s"`+10))}" | base64)
|
||||
k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36"
|
||||
s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1)
|
||||
token="$h64.$p64.$s"
|
||||
```
|
||||
78
api_auth_docker/auth.sh
Normal file
78
api_auth_docker/auth.sh
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# This is not designed to serve thousands of API key!
|
||||
#
|
||||
# header = {"alg":"HS256","typ":"JWT"}
|
||||
# header64 = base64(header) = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==
|
||||
#
|
||||
# payload = {"id":"001","exp":1538528077}
|
||||
# payload64 = base64(payload) = eyJpZCI6IjAwMSIsImV4cCI6MTUzODUyODA3N30K
|
||||
#
|
||||
# signature = hmacsha256(header64.payload64, key)
|
||||
#
|
||||
# token = header64.payload64.signature = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==.eyJpZCI6IjAwMSIsImV4cCI6MTUzODUyODA3N30K.signature
|
||||
#
|
||||
|
||||
. ./trace.sh
|
||||
|
||||
verify()
|
||||
{
|
||||
local header64=$(echo ${1} | cut -sd '.' -f1)
|
||||
local payload64=$(echo ${1} | cut -sd '.' -f2)
|
||||
local signature=$(echo ${1} | cut -sd '.' -f3)
|
||||
|
||||
trace "[verify] header64=${header64}"
|
||||
trace "[verify] payload64=${payload64}"
|
||||
trace "[verify] signature=${signature}"
|
||||
|
||||
local payload=$(echo ${payload64} | base64 -d)
|
||||
local exp=$(echo ${payload} | jq ".exp")
|
||||
local current=$(date +"%s")
|
||||
|
||||
trace "[verify] payload=${payload}"
|
||||
trace "[verify] exp=${exp}"
|
||||
trace "[verify] current=${current}"
|
||||
|
||||
if [ ${exp} -gt ${current} ]; then
|
||||
trace "[verify] Not expired, let's validate signature"
|
||||
local id=$(echo ${payload} | jq ".id" | tr -d '"')
|
||||
trace "[verify] id=${id}"
|
||||
|
||||
# It is so much faster to include the keys here instead of grep'ing the file for key.
|
||||
. ./keys.properties
|
||||
|
||||
local key
|
||||
eval key='$key'$id
|
||||
trace "[verify] key=${key}"
|
||||
local comp_sign=$(echo "${header64}.${payload64}" | openssl dgst -hmac "${key}" -sha256 -r | cut -sd ' ' -f1)
|
||||
|
||||
trace "[verify] comp_sign=${comp_sign}"
|
||||
|
||||
if [ "${comp_sign}" = "${signature}" ]; then
|
||||
trace "[verify] Valid signature!"
|
||||
echo -en "Status: 200 OK\r\n\r\n"
|
||||
return
|
||||
fi
|
||||
trace "[verify] Invalid signature!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
trace "[verify] Expired!"
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# $HTTP_AUTHORIZATION = Bearer <token>
|
||||
trace "[auth.sh] HTTP_AUTHORIZATION=${HTTP_AUTHORIZATION}"
|
||||
if [ "${HTTP_AUTHORIZATION:0:6}" = "Bearer" ]; then
|
||||
token="${HTTP_AUTHORIZATION:6}"
|
||||
|
||||
if [ -n "$token" ]; then
|
||||
trace "[auth.sh] Valid format for authorization header"
|
||||
verify "${token}"
|
||||
[ "$?" -eq "0" ] && return
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -en "Status: 403 Forbidden\r\n\r\n"
|
||||
29
api_auth_docker/default-ssl.conf
Normal file
29
api_auth_docker/default-ssl.conf
Normal file
@@ -0,0 +1,29 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name localhost;
|
||||
|
||||
ssl_certificate /etc/ssl/certs/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/private/key.pem;
|
||||
|
||||
location / {
|
||||
auth_request /auth;
|
||||
proxy_pass http://cyphernode:8888;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
internal;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /etc/nginx/conf.d/auth.sh;
|
||||
fastcgi_pass unix:/var/run/fcgiwrap.socket;
|
||||
}
|
||||
|
||||
#error_page 404 /404.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
26
api_auth_docker/default.conf
Normal file
26
api_auth_docker/default.conf
Normal file
@@ -0,0 +1,26 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
auth_request /auth;
|
||||
proxy_pass http://cyphernode:8888;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
internal;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /etc/nginx/conf.d/auth.sh;
|
||||
fastcgi_pass unix:/var/run/fcgiwrap.socket;
|
||||
}
|
||||
|
||||
#error_page 404 /404.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
5
api_auth_docker/entrypoint.sh
Normal file
5
api_auth_docker/entrypoint.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
spawn-fcgi -s /var/run/fcgiwrap.socket -u nginx -g nginx -U nginx -- /usr/bin/fcgiwrap
|
||||
|
||||
nginx -g "daemon off;"
|
||||
3
api_auth_docker/keys.properties
Normal file
3
api_auth_docker/keys.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
#keyid=hex(key)
|
||||
key001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36
|
||||
key002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83
|
||||
15
api_auth_docker/trace.sh
Normal file
15
api_auth_docker/trace.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
trace()
|
||||
{
|
||||
if [ -n "${TRACING}" ]; then
|
||||
echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr
|
||||
fi
|
||||
}
|
||||
|
||||
trace_rc()
|
||||
{
|
||||
if [ -n "${TRACING}" ]; then
|
||||
echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr
|
||||
fi
|
||||
}
|
||||
Reference in New Issue
Block a user