@webda/hawk
Version:
Implements Hawk on webda
228 lines • 8.81 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Cache, Inject, Service, ServiceParameters, WebdaError } from "@webda/core";
import * as Hawk from "hawk";
/**
*
*/
export class HawkServiceParameters extends ServiceParameters {
/**
* @inheritdoc
*/
constructor(params) {
super(params);
this.redirectUris ?? (this.redirectUris = []);
}
}
/**
* Verify signature and sign server
*
* Implementation of hawk protocol
* https://github.com/mozilla/hawk#readme
*
* @WebdaModda Hawk
*/
class HawkService extends Service {
/**
* @inheritdoc
*/
loadParameters(params) {
return new HawkServiceParameters(params);
}
/**
*
* @param id
* @param _timestamp used to invalidate cache
* @returns
*/
async getApiKey(id, _timestamp = undefined) {
return (await this.model.ref(id).get()).toHawkCredentials();
}
/**
* Return information for hawk
*/
async getHawkRequest(context) {
let http = context.getHttpContext();
return {
method: http.getMethod(),
url: http.getUrl(),
host: http.getHostName(),
port: http.getPortNumber(),
authorization: http.getUniqueHeader("authorization"),
payload: (await http.getRawBody()) || "",
contentType: http.getUniqueHeader("content-type") || ""
};
}
/**
* Add the Request listeners
*/
async init() {
await super.init();
this.model = this.parameters.keyModel ? this.getWebda().getModel(this.parameters.keyModel) : undefined;
if (!this.model && !this.parameters.dynamicSessionKey) {
throw new Error("Model must exists or dynamic session key must be defined");
}
if (this.model) {
// Make sure origins exist
await this.getOrigins();
}
this.getWebda().registerCORSFilter(this);
this.getWebda().registerRequestFilter(this);
// Solution to get an CSRF token
if (this.parameters.redirectUrl) {
this.addRoute(this.parameters.redirectUrl + "{?url}", ["GET"], this._redirect);
}
// Manage hawk server signature
this.getWebda().on("Webda.Result", async ({ context }) => {
try {
// Flushed headers
if (context.hasFlushedHeaders()) {
return;
}
const headers = context.getResponseHeaders();
const contentType = headers["Content-Type"] || headers["content-type"] || "application/json";
// Send current time to be able to detect any time synchronization issue
context.setHeader("x-server-time", Date.now());
const hawkContext = context.getExtension("hawk");
if (hawkContext === undefined) {
return;
}
const header = Hawk.server.header(hawkContext.credentials, hawkContext.artifacts, {
payload: context.getResponseBody(),
contentType
});
// LambdaServer behave a bit different
context.setHeader("Server-Authorization", header);
}
catch (err) {
this.log("TRACE", `Hawk init failed : '${err.message}'`);
}
});
return this;
}
/**
* Redirect with a CSRF
* @param context
*/
async _redirect(context) {
return this.redirectWithCSRF(context, context.getParameters().url);
}
/**
* Redirect to a website with CSRF
*/
async redirectWithCSRF(context, url) {
var _a, _b;
if (!this.parameters.redirectUris.some(u => url.startsWith(u))) {
throw new WebdaError.Forbidden("Invalid redirect");
}
(_a = context.getSession())[_b = this.parameters.dynamicSessionKey] ?? (_a[_b] = `${this.cryptoService.current}.${this.getWebda().getUuid("base64")}`);
let updatedUrl = new URL(url);
const [key, data] = context.getSession()[this.parameters.dynamicSessionKey].split(".");
updatedUrl.searchParams.set("csrf", await this.cryptoService.hmac(data, key));
context.redirect(updatedUrl.toString());
}
async getOrigins() {
const registry = this.getWebda().getRegistry();
let origin = await registry.get(HawkService.RegistryEntry);
if (origin === undefined && this.model) {
origin = await registry.save({ uuid: HawkService.RegistryEntry });
}
return origin;
}
async checkOPTIONS(origin) {
let origins = await this.getOrigins();
for (let key of Object.keys(origins).filter(n => n.startsWith("key_"))) {
// Origin is strictly matched by string search
if (origins[key].statics.indexOf(origin) > -1) {
return true;
}
// Origin can be matched by a special regexp too
for (let pattern of origins[key].patterns) {
if (origin.match(pattern)) {
return true;
}
}
}
return false;
}
/**
* Stricly parse the request's attributes and then approve or reject
* @param {Context} context
* @returns {Promise<boolean>}
*/
async checkRequest(context) {
// Authorize the options
if (context.getHttpContext().getMethod() === "OPTIONS") {
return this.checkOPTIONS(context.getHttpContext().getUniqueHeader("origin") || "");
}
// Only check Hawk
let authorization = context.getHttpContext().getUniqueHeader("authorization");
if (!authorization || !authorization.startsWith("Hawk ")) {
return false;
}
// We already check through the CORS part
if (context.getExtension("HawkReviewed")) {
return true;
}
context.setExtension("HawkReviewed", true);
const hawkRequest = await this.getHawkRequest(context);
// Specific dynamic session checks (useful for CSRF token)
if (this.parameters.dynamicSessionKey && authorization.startsWith('Hawk id="session"')) {
try {
if (!context.getSession()[this.parameters.dynamicSessionKey]) {
throw new WebdaError.Forbidden("No session key");
}
const [key, data] = context.getSession()[this.parameters.dynamicSessionKey].split(".");
context.setExtension("hawk", await Hawk.server.authenticate(hawkRequest, async () => ({
id: "session",
key: await this.cryptoService.hmac(data, key),
algorithm: "sha256"
})));
}
catch (err) {
if (err instanceof WebdaError.Forbidden) {
throw err;
}
throw new WebdaError.Forbidden(`Hawk error (${err.message})`);
}
}
else if (this.model) {
// We have an Api Key store
try {
context.setExtension("hawk", await Hawk.server.authenticate(hawkRequest, this.getApiKey.bind(this)));
let fullKey = await this.model.ref(context.getExtension("hawk").credentials.id).get();
if (!(await fullKey.canRequest(context))) {
throw new WebdaError.Forbidden("Key not allowed to request");
}
}
catch (err) {
this.log("TRACE", err);
if (err instanceof WebdaError.Forbidden) {
throw err;
}
throw new WebdaError.Forbidden("Bad Hawk credentials");
}
}
return true;
}
}
HawkService.RegistryEntry = "HawkOrigins";
export default HawkService;
__decorate([
Inject("CryptoService")
], HawkService.prototype, "cryptoService", void 0);
__decorate([
Cache()
], HawkService.prototype, "getApiKey", null);
__decorate([
Cache()
], HawkService.prototype, "getOrigins", null);
__decorate([
Cache()
], HawkService.prototype, "checkOPTIONS", null);
export { HawkService };
//# sourceMappingURL=hawk.js.map