Make auth use JWTs.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3991 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
parent
5b3ae3f006
commit
113a82b382
94
core/auth.js
94
core/auth.js
@ -5,38 +5,66 @@ import * as form from './form.js';
|
|||||||
var gTokens = {};
|
var gTokens = {};
|
||||||
var gDatabase = new Database("auth");
|
var gDatabase = new Database("auth");
|
||||||
|
|
||||||
|
const kRefreshInterval = 1 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function b64url(value) {
|
||||||
|
value = value.replaceAll('+', '-').replaceAll('/', '_');
|
||||||
|
let equals = value.indexOf('=');
|
||||||
|
if (equals !== -1) {
|
||||||
|
return value.substring(0, equals);
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unb64url(value) {
|
||||||
|
value = value.replaceAll('-', '+').replaceAll('_', '/');
|
||||||
|
let remainder = value.length % 4;
|
||||||
|
if (remainder == 3) {
|
||||||
|
return value + '=';
|
||||||
|
} else if (remainder == 2) {
|
||||||
|
return value + '==';
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeJwt(payload) {
|
||||||
|
let ids = ssb.getIdentities(':auth');
|
||||||
|
let id;
|
||||||
|
if (ids?.length) {
|
||||||
|
id = ids[0];
|
||||||
|
} else {
|
||||||
|
id = ssb.createIdentity(':auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
let final_payload = b64url(base64Encode(JSON.stringify(Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval}))));
|
||||||
|
let jwt = [b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))), final_payload, b64url(ssb.hmacsha256sign(final_payload, ':auth', id))].join('.');
|
||||||
|
return jwt;
|
||||||
|
}
|
||||||
|
|
||||||
function readSession(session) {
|
function readSession(session) {
|
||||||
var result = session ? gDatabase.get("session:" + session) : null;
|
let jwt_parts = session?.split('.');
|
||||||
|
if (jwt_parts?.length === 3) {
|
||||||
if (result) {
|
let [header, payload, signature] = jwt_parts;
|
||||||
result = JSON.parse(result);
|
header = JSON.parse(base64Decode(unb64url(header)));
|
||||||
|
if (header.typ === 'JWT' && header.alg === 'HS256') {
|
||||||
let kRefreshInterval = 1 * 60 * 60 * 1000;
|
signature = unb64url(signature);
|
||||||
let now = Date.now();
|
let id = ssb.getIdentities(':auth');
|
||||||
if (!result.lastAccess || result.lastAccess < now - kRefreshInterval) {
|
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
|
||||||
result.lastAccess = now;
|
let result = JSON.parse(base64Decode(unb64url(payload)));
|
||||||
writeSession(session, result);
|
if ((new Date()).valueOf() < result.exp) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
} else {
|
||||||
|
print('JWT expired.');
|
||||||
function writeSession(session, value) {
|
}
|
||||||
gDatabase.set("session:" + session, JSON.stringify(value));
|
} else {
|
||||||
}
|
print('JWT verification failed.');
|
||||||
|
}
|
||||||
function removeSession(session, value) {
|
} else {
|
||||||
gDatabase.remove("session:" + session);
|
print('Invalid JWT header.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function newSession() {
|
|
||||||
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
var result = "";
|
|
||||||
for (var i = 0; i < 32; i++) {
|
|
||||||
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyPassword(password, hash) {
|
function verifyPassword(password, hash) {
|
||||||
@ -92,7 +120,6 @@ function handler(request, response) {
|
|||||||
var formData = form.decodeForm(request.query);
|
var formData = form.decodeForm(request.query);
|
||||||
|
|
||||||
if (request.method == "POST" || formData.submit) {
|
if (request.method == "POST" || formData.submit) {
|
||||||
session = newSession();
|
|
||||||
sessionIsNew = true;
|
sessionIsNew = true;
|
||||||
formData = form.decodeForm(utf8Decode(request.body), formData);
|
formData = form.decodeForm(utf8Decode(request.body), formData);
|
||||||
if (formData.submit == "Login") {
|
if (formData.submit == "Login") {
|
||||||
@ -114,7 +141,7 @@ function handler(request, response) {
|
|||||||
if (users !== users_original) {
|
if (users !== users_original) {
|
||||||
gDatabase.set('users', users);
|
gDatabase.set('users', users);
|
||||||
}
|
}
|
||||||
writeSession(session, {name: formData.name});
|
session = makeJwt({name: formData.name});
|
||||||
account = {password: hashPassword(formData.password)};
|
account = {password: hashPassword(formData.password)};
|
||||||
gDatabase.set("user:" + formData.name, JSON.stringify(account));
|
gDatabase.set("user:" + formData.name, JSON.stringify(account));
|
||||||
if (noAdministrator()) {
|
if (noAdministrator()) {
|
||||||
@ -127,7 +154,7 @@ function handler(request, response) {
|
|||||||
if (account &&
|
if (account &&
|
||||||
account.password &&
|
account.password &&
|
||||||
verifyPassword(formData.password, account.password)) {
|
verifyPassword(formData.password, account.password)) {
|
||||||
writeSession(session, {name: formData.name});
|
session = makeJwt({name: formData.name});
|
||||||
if (noAdministrator()) {
|
if (noAdministrator()) {
|
||||||
makeAdministrator(formData.name);
|
makeAdministrator(formData.name);
|
||||||
}
|
}
|
||||||
@ -137,7 +164,7 @@ function handler(request, response) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Proceed as Guest
|
// Proceed as Guest
|
||||||
writeSession(session, {name: "guest"});
|
session = makeJwt({name: 'guest'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,7 +218,6 @@ function handler(request, response) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (request.uri == "/login/logout") {
|
} else if (request.uri == "/login/logout") {
|
||||||
removeSession(session);
|
|
||||||
response.writeHead(303, {"Set-Cookie": "session=; path=/; secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")});
|
response.writeHead(303, {"Set-Cookie": "session=; path=/; secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")});
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
|
75
src/ssb.js.c
75
src/ssb.js.c
@ -7,6 +7,7 @@
|
|||||||
#include "task.h"
|
#include "task.h"
|
||||||
#include "util.js.h"
|
#include "util.js.h"
|
||||||
|
|
||||||
|
#include <base64c.h>
|
||||||
#include <sodium/crypto_hash_sha256.h>
|
#include <sodium/crypto_hash_sha256.h>
|
||||||
#include <sodium/crypto_sign.h>
|
#include <sodium/crypto_sign.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@ -815,6 +816,78 @@ static JSValue _tf_ssb_remove_event_listener(JSContext* context, JSValueConst th
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static JSValue _tf_ssb_hmacsha256_sign(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||||
|
{
|
||||||
|
JSValue result = JS_UNDEFINED;
|
||||||
|
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
|
||||||
|
|
||||||
|
size_t payload_length = 0;
|
||||||
|
const char* payload = JS_ToCStringLen(context, &payload_length, argv[0]);
|
||||||
|
const char* user = JS_ToCString(context, argv[1]);
|
||||||
|
const char* public_key = JS_ToCString(context, argv[2]);
|
||||||
|
|
||||||
|
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
|
||||||
|
if (tf_ssb_db_identity_get_private_key(ssb, user, public_key, private_key, sizeof(private_key)))
|
||||||
|
{
|
||||||
|
uint8_t signature[crypto_sign_BYTES];
|
||||||
|
unsigned long long siglen;
|
||||||
|
if (crypto_sign_detached(signature, &siglen, (const uint8_t*)payload, payload_length, private_key) == 0)
|
||||||
|
{
|
||||||
|
char signature_base64[crypto_sign_BYTES * 2];
|
||||||
|
base64c_encode(signature, sizeof(signature), (uint8_t*)signature_base64, sizeof(signature_base64));
|
||||||
|
result = JS_NewString(context, signature_base64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = JS_ThrowInternalError(context, "Private key not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_FreeCString(context, public_key);
|
||||||
|
JS_FreeCString(context, user);
|
||||||
|
JS_FreeCString(context, payload);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JSValue _tf_ssb_hmacsha256_verify(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||||
|
{
|
||||||
|
JSValue result = JS_UNDEFINED;
|
||||||
|
|
||||||
|
size_t public_key_length = 0;
|
||||||
|
const char* public_key = JS_ToCStringLen(context, &public_key_length, argv[0]);
|
||||||
|
size_t payload_length = 0;
|
||||||
|
const char* payload = JS_ToCStringLen(context, &payload_length, argv[1]);
|
||||||
|
size_t signature_length = 0;
|
||||||
|
const char* signature = JS_ToCStringLen(context, &signature_length, argv[2]);
|
||||||
|
|
||||||
|
const char* public_key_start = public_key && *public_key == '@' ? public_key + 1 : public_key;
|
||||||
|
const char* public_key_end = strstr(public_key_start, ".ed25519");
|
||||||
|
if (!public_key_end)
|
||||||
|
{
|
||||||
|
public_key_end = public_key_start + strlen(public_key_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t bin_public_key[crypto_sign_PUBLICKEYBYTES] = { 0 };
|
||||||
|
if (base64c_decode((const uint8_t*)public_key_start, public_key_end - public_key_start, bin_public_key, sizeof(bin_public_key)) > 0)
|
||||||
|
{
|
||||||
|
uint8_t bin_signature[crypto_sign_BYTES] = { 0 };
|
||||||
|
if (base64c_decode((const uint8_t*)signature, signature_length, bin_signature, sizeof(bin_signature)) > 0)
|
||||||
|
{
|
||||||
|
if (crypto_sign_verify_detached(bin_signature, (const uint8_t*)payload, payload_length, bin_public_key) == 0)
|
||||||
|
{
|
||||||
|
result = JS_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_FreeCString(context, signature);
|
||||||
|
JS_FreeCString(context, payload);
|
||||||
|
JS_FreeCString(context, public_key);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
|
void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
|
||||||
{
|
{
|
||||||
JS_NewClassID(&_tf_ssb_classId);
|
JS_NewClassID(&_tf_ssb_classId);
|
||||||
@ -836,6 +909,8 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
|
|||||||
JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1));
|
JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1));
|
||||||
JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1));
|
JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1));
|
||||||
JS_SetPropertyStr(context, object, "appendMessageWithIdentity", JS_NewCFunction(context, _tf_ssb_appendMessageWithIdentity, "appendMessageWithIdentity", 3));
|
JS_SetPropertyStr(context, object, "appendMessageWithIdentity", JS_NewCFunction(context, _tf_ssb_appendMessageWithIdentity, "appendMessageWithIdentity", 3));
|
||||||
|
JS_SetPropertyStr(context, object, "hmacsha256sign", JS_NewCFunction(context, _tf_ssb_hmacsha256_sign, "hmacsha256sign", 3));
|
||||||
|
JS_SetPropertyStr(context, object, "hmacsha256verify", JS_NewCFunction(context, _tf_ssb_hmacsha256_verify, "hmacsha256verify", 3));
|
||||||
|
|
||||||
/* Does not require an identity. */
|
/* Does not require an identity. */
|
||||||
JS_SetPropertyStr(context, object, "getAllIdentities", JS_NewCFunction(context, _tf_ssb_getAllIdentities, "getAllIdentities", 0));
|
JS_SetPropertyStr(context, object, "getAllIdentities", JS_NewCFunction(context, _tf_ssb_getAllIdentities, "getAllIdentities", 0));
|
||||||
|
57
src/tests.c
57
src/tests.c
@ -585,6 +585,61 @@ static void _test_file(const tf_test_options_t* options)
|
|||||||
unlink("out/test.js");
|
unlink("out/test.js");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void _test_sign(const tf_test_options_t* options)
|
||||||
|
{
|
||||||
|
FILE* file = fopen("out/test.js", "w");
|
||||||
|
fprintf(file,
|
||||||
|
"'use strict';\n"
|
||||||
|
"let id = ssb.createIdentity('test');\n"
|
||||||
|
"print(id);\n"
|
||||||
|
"let sig = ssb.hmacsha256sign('hello', 'test', id);\n"
|
||||||
|
"print(sig);\n"
|
||||||
|
"if (!ssb.hmacsha256verify(id, 'hello', sig)) {\n"
|
||||||
|
" exit(1);\n"
|
||||||
|
"}\n"
|
||||||
|
"if (ssb.hmacsha256verify(id, 'world', sig)) {\n"
|
||||||
|
" exit(1);\n"
|
||||||
|
"}\n"
|
||||||
|
"if (ssb.hmacsha256verify(id, 'hello1', sig)) {\n"
|
||||||
|
" exit(1);\n"
|
||||||
|
"}\n"
|
||||||
|
);
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
char command[256];
|
||||||
|
snprintf(command, sizeof(command), "%s run --ssb-port=0 -s out/test.js", options->exe_path);
|
||||||
|
printf("%s\n", command);
|
||||||
|
int result = system(command);
|
||||||
|
printf("returned %d\n", WEXITSTATUS(result));
|
||||||
|
assert(WIFEXITED(result));
|
||||||
|
assert(WEXITSTATUS(result) == 0);
|
||||||
|
|
||||||
|
unlink("out/test.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _test_b64(const tf_test_options_t* options)
|
||||||
|
{
|
||||||
|
FILE* file = fopen("out/test.js", "w");
|
||||||
|
fprintf(file,
|
||||||
|
"'use strict';\n"
|
||||||
|
"print(base64Encode('hello'));\n"
|
||||||
|
"if (base64Decode(base64Encode('hello')) !== 'hello') {\n"
|
||||||
|
" exit(1);\n"
|
||||||
|
"}\n"
|
||||||
|
);
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
char command[256];
|
||||||
|
snprintf(command, sizeof(command), "%s run --ssb-port=0 -s out/test.js", options->exe_path);
|
||||||
|
printf("%s\n", command);
|
||||||
|
int result = system(command);
|
||||||
|
printf("returned %d\n", WEXITSTATUS(result));
|
||||||
|
assert(WIFEXITED(result));
|
||||||
|
assert(WEXITSTATUS(result) == 0);
|
||||||
|
|
||||||
|
unlink("out/test.js");
|
||||||
|
}
|
||||||
|
|
||||||
static void _tf_test_run(const tf_test_options_t* options, const char* name, void (*test)(const tf_test_options_t* options))
|
static void _tf_test_run(const tf_test_options_t* options, const char* name, void (*test)(const tf_test_options_t* options))
|
||||||
{
|
{
|
||||||
bool specified = false;
|
bool specified = false;
|
||||||
@ -639,5 +694,7 @@ void tf_tests(const tf_test_options_t* options)
|
|||||||
_tf_test_run(options, "uint8array", _test_uint8array);
|
_tf_test_run(options, "uint8array", _test_uint8array);
|
||||||
_tf_test_run(options, "socket", _test_socket);
|
_tf_test_run(options, "socket", _test_socket);
|
||||||
_tf_test_run(options, "file", _test_file);
|
_tf_test_run(options, "file", _test_file);
|
||||||
|
_tf_test_run(options, "sign", _test_sign);
|
||||||
|
_tf_test_run(options, "b64", _test_b64);
|
||||||
printf("Tests completed.\n");
|
printf("Tests completed.\n");
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
#include "task.h"
|
#include "task.h"
|
||||||
#include "trace.h"
|
#include "trace.h"
|
||||||
|
|
||||||
#include "quickjs-libc.h"
|
#include <base64c.h>
|
||||||
|
#include <quickjs-libc.h>
|
||||||
#include <uv.h>
|
#include <uv.h>
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@ -65,6 +65,42 @@ JSValue tf_util_utf8_decode(JSContext* context, JSValue value)
|
|||||||
return _util_utf8_decode(context, JS_NULL, 1, &value);
|
return _util_utf8_decode(context, JS_NULL, 1, &value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static JSValue _util_base64_encode(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||||
|
{
|
||||||
|
JSValue result = JS_UNDEFINED;
|
||||||
|
size_t length = 0;
|
||||||
|
const char* value = JS_ToCStringLen(context, &length, argv[0]);
|
||||||
|
char* encoded = tf_malloc(length * 4);
|
||||||
|
|
||||||
|
int r = base64c_encode((const uint8_t*)value, length, (uint8_t*)encoded, length * 4);
|
||||||
|
if (r >= 0)
|
||||||
|
{
|
||||||
|
result = JS_NewStringLen(context, encoded, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
tf_free(encoded);
|
||||||
|
JS_FreeCString(context, value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JSValue _util_base64_decode(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||||
|
{
|
||||||
|
JSValue result = JS_UNDEFINED;
|
||||||
|
size_t length = 0;
|
||||||
|
const char* value = JS_ToCStringLen(context, &length, argv[0]);
|
||||||
|
char* encoded = tf_malloc(length);
|
||||||
|
|
||||||
|
int r = base64c_decode((const uint8_t*)value, length, (uint8_t*)encoded, length);
|
||||||
|
if (r >= 0)
|
||||||
|
{
|
||||||
|
result = JS_NewStringLen(context, encoded, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
tf_free(encoded);
|
||||||
|
JS_FreeCString(context, value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t* tf_util_try_get_array_buffer(JSContext* context, size_t* psize, JSValueConst obj)
|
uint8_t* tf_util_try_get_array_buffer(JSContext* context, size_t* psize, JSValueConst obj)
|
||||||
{
|
{
|
||||||
uint8_t* result = JS_GetArrayBuffer(context, psize, obj);
|
uint8_t* result = JS_GetArrayBuffer(context, psize, obj);
|
||||||
@ -197,6 +233,8 @@ void tf_util_register(JSContext* context)
|
|||||||
JSValue global = JS_GetGlobalObject(context);
|
JSValue global = JS_GetGlobalObject(context);
|
||||||
JS_SetPropertyStr(context, global, "utf8Decode", JS_NewCFunction(context, _util_utf8_decode, "utf8Decode", 1));
|
JS_SetPropertyStr(context, global, "utf8Decode", JS_NewCFunction(context, _util_utf8_decode, "utf8Decode", 1));
|
||||||
JS_SetPropertyStr(context, global, "utf8Encode", JS_NewCFunction(context, _util_utf8_encode, "utf8Encode", 1));
|
JS_SetPropertyStr(context, global, "utf8Encode", JS_NewCFunction(context, _util_utf8_encode, "utf8Encode", 1));
|
||||||
|
JS_SetPropertyStr(context, global, "base64Decode", JS_NewCFunction(context, _util_base64_decode, "base64Decode", 1));
|
||||||
|
JS_SetPropertyStr(context, global, "base64Encode", JS_NewCFunction(context, _util_base64_encode, "base64Encode", 1));
|
||||||
JS_SetPropertyStr(context, global, "print", JS_NewCFunction(context, _util_print, "print", 1));
|
JS_SetPropertyStr(context, global, "print", JS_NewCFunction(context, _util_print, "print", 1));
|
||||||
JS_SetPropertyStr(context, global, "setTimeout", JS_NewCFunction(context, _util_setTimeout, "setTimeout", 2));
|
JS_SetPropertyStr(context, global, "setTimeout", JS_NewCFunction(context, _util_setTimeout, "setTimeout", 2));
|
||||||
JS_FreeValue(context, global);
|
JS_FreeValue(context, global);
|
||||||
|
Loading…
Reference in New Issue
Block a user