I'm trying to make a GitHub webhook server with Deno, but I cannot find any possible way to do the validation.
This is my current attempt using webhooks-methods.js:
import { Application } from "https://deno.land/x/oak/mod.ts";
import { verify } from "https://cdn.skypack.dev/#octokit/webhooks-methods?dts";
const app = new Application();
app.use(async (ctx, next) => {
try {
await next();
} catch (_err) {
ctx.response.status = 500;
}
});
const secret = "...";
app.use(async (ctx) => {
const signature = ctx.request.headers.get("X-Hub-Signature-256");
if (signature) {
const payload = await ctx.request.body({ type: "text" }).value;
const result = await verify(secret, payload, signature);
console.log(result);
}
ctx.response.status = 200;
});
The verify function is returning false every time.
Your example is very close. The GitHub webhook documentation details the signature header schema. The value is a digest algorithm prefix followed by the signature, in the format of ${ALGO}=${SIGNATURE}:
X-Hub-Signature-256: sha256=d57c68ca6f92289e6987922ff26938930f6e66a2d161ef06abdf1859230aa23c
So, you need to extract the signature from the value (omitting the prefix):
const signatureHeader = request.headers.get("X-Hub-Signature-256");
const signature = signatureHeader.slice("sha256=".length);
Update: Starting in release version 3.0.1 of octokit/webhooks-methods.js, it is no longer necessary to manually extract the signature from the header — that task is handled by the verify function. The code in the answer has been updated to reflect this change.
Here's a complete, working example that you can simply copy + paste into a project or playground on Deno Deploy:
gh-webhook-logger.ts:
import { assert } from "https://deno.land/std#0.177.0/testing/asserts.ts";
import {
Application,
NativeRequest,
Router,
} from "https://deno.land/x/oak#v11.1.0/mod.ts";
import type { ServerRequest } from "https://deno.land/x/oak#v11.1.0/types.d.ts";
import { verify } from "https://esm.sh/#octokit/webhooks-methods#3.0.2?pin=v106";
// In actual usage, use a private secret:
// const SECRET = Deno.env.get("SIGNING_SECRET");
// But for the purposes of this demo, the exposed secret is:
const SECRET = "Let me know if you found this to be helpful!";
type GitHubWebhookVerificationStatus = {
id: string;
verified: boolean;
};
// Because this uses a native Request,
// it can be used in other contexts besides Oak (e.g. `std/http/serve`):
async function verifyGitHubWebhook(
request: Request,
): Promise<GitHubWebhookVerificationStatus> {
const id = request.headers.get("X-GitHub-Delivery");
// This should be more strict in reality
assert(id, "Not a GH webhhok");
const signatureHeader = request.headers.get("X-Hub-Signature-256");
let verified = false;
if (signatureHeader) {
const payload = await request.clone().text();
verified = await verify(SECRET, payload, signatureHeader);
}
return { id, verified };
}
// Type predicate used to access native Request instance
// Ref: https://github.com/oakserver/oak/issues/501#issuecomment-1084046581
function isNativeRequest(r: ServerRequest): r is NativeRequest {
// deno-lint-ignore no-explicit-any
return (r as any).request instanceof Request;
}
const webhookLogger = new Router().post("/webhook", async (ctx) => {
assert(isNativeRequest(ctx.request.originalRequest));
const status = await verifyGitHubWebhook(ctx.request.originalRequest.request);
console.log(status);
ctx.response.status = 200;
});
const app = new Application()
.use(webhookLogger.routes())
.use(webhookLogger.allowedMethods());
// The port is not important in Deno Deploy
await app.listen({ port: 8080 });
Related
Is there a recommended pattern in Remix for running common code on every request, and potentially adding context data to the request? Like a middleware? A usecase for this might be to do logging or auth, for example.
The one thing I've seen that seems similar to this is loader context via the getLoadContext API. This lets you populate a context object which is passed as an arg to all route loaders.
It does work, and initially seems like the way to do this, but the docs for it say...
It's a way to bridge the gap between the adapter's request/response API with your Remix app
This API is an escape hatch, it’s uncommon to need it
...which makes me think otherwise, because
This API is explicitly for custom integrations with the server runtime. But it doesn't seem like middlewares should be specific to the server runtime - they should just be part of the 'application' level as a Remix feature.
Running middlewares is a pretty common pattern in web frameworks!
So, does Remix have any better pattern for middleware that runs before every loader?
Instead of middleware, you can call a function directly inside the loader, this will also be more explicit. If you want to early return a response from those "middlewares" Remix let you throw the response object.
For example, if you wanted to check the user has a certain role you could create this function:
async function verifyUserRole(request: Request, expectedRole: string) {
let user = await getAuthenticatedUser(request); // somehow get the user
if (user.role === expectedRole) return user;
throw json({ message: "Forbidden" }, { status: 403 });
}
And in any loader call it this way:
let loader: LoaderFunction = async ({ request }) => {
let user = await verifyUserRole(request, "admin");
// code here will only run if user is an admin
// and you'll also get the user object at the same time
};
Another example could be to require HTTPS
function requireHTTPS(request: Request) {
let url = new URL(request.url);
if (url.protocol === "https:") return;
url.protocol = "https:";
throw redirect(url.toString());
}
let loader: LoaderFunction = async ({ request }) => {
await requireHTTPS(request);
// run your loader (or action) code here
};
There is no way inside Remix to run code before loaders.
As you found out, there is the loader context but it runs even before remix starts to do its job (so you won't know which route modules are matched for example).
You can also run arbitrary code before handing the request to remix in the JS file where you use the adapter for the platform you're deploying to (this depend on the starter you used. This file doesn't exist if you've chosen remix server as your server)
For now it should work for some use cases, but I agree this is a missing feature in remix for now.
Inside app/root.tsx
export let loader: LoaderFunction = ({ request }) => {
const url = new URL(request.url);
const hostname = url.hostname;
const proto = request.headers.get("X-Forwarded-Proto") ?? url.protocol;
url.host =
request.headers.get("X-Forwarded-Host") ??
request.headers.get("host") ??
url.host;
url.protocol = "https:";
if (proto === "http" && hostname !== "localhost") {
return redirect(url.toString(), {
headers: {
"X-Forwarded-Proto": "https",
},
});
}
return {};
};
Source: https://github.com/remix-run/remix-jokes/blob/8f786d9d7fa7ea62203e87c1e0bdaa9bda3b28af/app/root.tsx#L25-L46
here is my middlewares implementation for remix with typescript,it's works well
ctx.return(something)=== useLoaderData()
import compose from '#utils/compose';
export default function Index() {
const ctx = useLoaderData();
return <div>{ctx.name}</div>;
}
type DefaultCtx = {
name: string;
} & Request;
export const loader =(...args)=>compose<DefaultCtx>(
async (ctx, next) => {
ctx.name = 'first';
await next();
},
async (ctx, next) => {
ctx.name = 'secnod';
await next();
},
async (ctx, next) => {
ctx.name = 'third';
ctx.return(ctx);
await next();
}
)(args);
compose is same as koa;
here is the compose's implementation
type Next = () => Promise<void>;
type Context = {};
type Middle<T = {}> = (ctx: Context & T, next: Next) => void;
const compose = <T>(...middlewares: Middle<T>[]) => {
return middlewares.reverse().reduce(
(dispatch, middleware) => {
return async ctx =>
middleware(ctx, async () => dispatch(ctx, async () => {}));
},
async () => {}
);
};
export type Middleware<T = {}, P = unknown> = (
ctx: Context & T & { return: (param: P) => void },
next: Next
) => void;
const returnEarly: Middleware = async (ctx, next) => {
return new Promise<any>(async resolve => {
ctx.return = resolve;
await next();
});
};
const componseWithReturn = <T>(...middlewares: Middleware<T>[]) =>
compose(returnEarly, ...middlewares) as (ctx: T) => void;
export default componseWithReturn;
I am able to acquire access token but not sure how to send messages because it requires a user and my app is a backend app(nodejs script). On graph explorer, it works.
The code snippet on graph explorer is:
const options = {
authProvider, //I need this value
};
const client = Client.init(options);
const chatMessage = {body: {content: '#.####.#'}};
await client.api('/teams/my-team-id/channels/my-channel-id/messages')
.post(chatMessage);
How do I get authProvider in nodejs?
I tried using MSALAuthenticationProviderOptions but there seems to be an issue (as mentioned in their github repo) by following these steps: https://www.npmjs.com/package/#microsoft/microsoft-graph-client.
You need to run this in the context of an application instead of a user. The Microsoft Graph JavaScript library now supports Azure TokenCredential for acquiring tokens.
const { Client } = require("#microsoft/microsoft-graph-client");
const { TokenCredentialAuthenticationProvider } = require("#microsoft/microsoft-graph-client/authProviders/azureTokenCredentials");
const { ClientSecretCredential } = require("#azure/identity");
const { clientId, clientSecret, scopes, tenantId } = require("./secrets"); // Manage your secret better than this please.
require("isomorphic-fetch");
async function runExample() {
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
const authProvider = new TokenCredentialAuthenticationProvider(credential, { scopes: [scopes] });
const client = Client.initWithMiddleware({
debugLogging: true,
authProvider,
});
const chatMessage = {body: {content: '#.####.#'}};
const res = await client.api('/teams/my-team-id/channels/my-channel-id/messages')
.post(chatMessage);
console.log(res);
}
runExample().catch((err) => {
console.log("Encountered an error:\n\n", err);
});
This sample came from:
https://github.com/microsoftgraph/msgraph-sdk-javascript/tree/dev/samples/tokenCredentialSamples/ClientCredentialFlow
I apologize if this is unclear, it's late and I don't know how best to explain it.
I'm using an event emitter to pass data from a server response to a function inside of a separate class in another file, but when trying to use methods in those classes, the this keyword obviously doesn't work (because in this scenario, this refers to the server event emitter) - how would I reference a function within the class itself? I've provided code to help illustrate my point a bit better
ServiceClass.js
class StreamService {
/**
*
* #param {} database
* #param {Collection<Guild>} guilds
*/
constructor (database, guilds,) {
.....
twitchListener.on('live', this.sendLiveAlert) // fire test method when we get a notification
// if there are streamers to monitor, being monitoring
winston.info('Stream service initialized')
}
..............
async get (url, params = null, headers = this.defaultHeaders) {
// check oauth token
const expirationDate = this.token.expires_in || 0
if (expirationDate <= Date.now() || !this.token) await this.getAccessToken()
// build URL
const index = 0
let paramsString = ''
for (const [key, value] of params.entries()) {
if (index === 0) {
paramsString += `?${key}=${value}`
} else {
paramsString += `&${key}=${value}`
}
}
const res = await fetch(url + paramsString, { method: 'GET', headers: headers })
if (!res.ok) {
winston.error(`Error performing GET request to ${url}`)
return null
}
return await res.json()
}
async sendLiveAlert(streamTitle, streamURL, avatar, userName, gameId, viewerCount, thumbnail, startDateTime) {
// get game name first (no headers needed)
const params = new Map()
params.set('id', gameId)
const gameData = await this.get('https://api.twitch.tv/heliix/games', params, this.defaultHeaders)
if(gameData) {
// get webhook and send message to channel
const webhookClient = new WebhookClient('755641606555697305', 'OWZvI01kUUf4AAIR9uv2z4CxRse3Ik8b0LKOluaOYKmhE33h0ypMLT0JJm3laomlZ05o')
const embed = new MessageEmbed()
.setTitle(`${userName} just went live on Twitch!`)
.setURL(streamURL)
.setThumbnail(avatar)
.addFields(
{ name: 'Now Playing', value: gameData.data[0].name },
{ name: 'Stream Title', value: streamTitle }
)
.setImage(thumbnail)
}
webhookClient.send('Webhook test', embed)
}
}
Server.js
class TwitchWebhookListener extends EventEmitter {
......................
// Routes
server
.post((req, res) => {
console.log('Incoming POST request on /webhooks')
............................
const data = req.body.data[0]
if(!this.streamerLiveStatus.get(data.user_id) && data.type === 'live') {
// pass request body to bot for processing
this.emit(
'live',
data.title, // stream title
`https://twitch.tv/${data.user_name}`, // channel link
`https://avatar.glue-bot.xyz/twitch/${data.user_name}`, // streamer avatar
data.user_name,
data.game_id,
data.viewer_count,
data.thumbnail_url,
data.started_at // do we need this?
)
}
break
default:
res.send(`Unknown webhook for ${req.params.id}`)
break
}
} else {
console.log('The Signature did not match')
res.send('Ok')
}
} else {
console.log('It didn\'t seem to be a Twitch Hook')
res.send('Ok')
}
})
}
}
const listener = new TwitchWebhookListener()
listener.listen()
module.exports = listener
Within the sendLiveAlert method, I'm trying to call the get method of the StreamService class - but because it's called directly via the emitter within server.js, this refers specifically to the Server.js class - is there any way I can use StreamService.get()? I could obviously just rewrite the code inside the method itself, but that seems unnecessary when its right there?
Change this:
twitchListener.on('live', this.sendLiveAlert)
to this:
twitchListener.on('live', this.sendLiveAlert.bind(this))
Or, you could also do this:
twitchListener.on('live', (...args) => {
this.sendLiveAlert(...args);
});
With .bind() it creates a function wrapper that resets the proper value of this for you. In the case of the arrow function, it preserves the lexical value of this for you.
People, how are you? I have a query, I just implemented my API made with apollo server in an AWS Lambda. I used the official documentation as a guide, but I'm noticing that the context handling varies a bit. I have a doubt with the latter, since I made certain changes and everything works fine locally using "serverless offline", but once I deploy it doesn't. Apparently the authentication context that I generate does not finish reaching my query. If someone can guide me a bit with this, I will be very grateful.
This is my API index:
const { ApolloServer, gql } = require('apollo-server-lambda');
const typeDefs = require('./db/schema');
const resolvers = require('./db/resolvers');
const db = require('./config/db');
const jwt = require('jsonwebtoken');
require('dotenv').config({ path: 'variables.env' });
db.conectDB();
// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
const server = new ApolloServer({
typeDefs,
resolvers,
playground: {
endpoint: "/graphql"
},
context: ({ event, context }) => {
try {
const token = event.headers['authorization'] || '';
if(token){
context.user = jwt.verify(token.replace('Bearer ',''), process.env.KEY_TOKEN);
}
return {
headers: event.headers,
functionName: context.functionName,
event,
context,
}
} catch (error) {
console.error(error);
}
}
});
exports.graphqlHandler = server.createHandler({
cors: {
origin: '*',
credentials: true,
},
});
This is my query:
getUserByToken: async (_, {}, { context }) => {
if(context)
throw new Error((context ? 'context' : '') + ' ' + (context.user ? 'user' : ''));
let user = await db.findOne('users',{ _id: ObjectId(context.user._id) });
if(user.birthdate)
user.birthdate = user.birthdate.toString();
if(user.password)
user.password = true;
else
user.password = false;
return user;
}
My API response:
API response
From what I can see, you're not calling getUserByToken in your context. Is that correct? So, I'm not sure how you're encountering this error.
Can I give you some pointers?
Connecting to your DB is probably (or it should be) asynchronous. For that, I'd run your code like this:
db.connect()
.then(() => {
... handle your request in here
})
.catch(console.error);
I think you meant to call your getUserByToken in this line:
context.user = jwt.verify(token.replace('Bearer ',''), process.env.KEY_TOKEN);
I'm trying to follow the GraphQL tutorial (https://www.apollographql.com/docs/tutorial/resolvers/), but I'm getting an error in the playground when I try to book a trip, with a stack trace starting like this:
"TypeError: Cannot read property 'id' of null",
" at UserAPI.bookTrips (/Users/kurtpeek/Documents/Scratch/fullstack-tutorial/start/server/src/datasources/user.js:35:38)",
In src/index.js, the ApolloServer (https://www.apollographql.com/docs/apollo-server/api/apollo-server/) is defined with an asynchronous context() function like so:
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const { createStore } = require('./utils');
const resolvers = require('./resolvers');
const LaunchAPI = require('./datasources/launch');
const UserAPI = require('./datasources/user');
const isEmail = require('isemail');
const store = createStore();
const server = new ApolloServer({
context: async ({ req }) => {
debugger;
const auth = (req.headers && req.headers.authorization) || '';
const email = Buffer.from(auth, 'base64').toString('ascii');
if (!isEmail.validate(email)) return { user: null };
const users = await store.users.findOrCreate({ where: { email }});
const user = users && users[0] ? users[0] : null;
return { user: { ...user.dataValues }};
},
typeDefs,
resolvers,
dataSources: () => ({
launchAPI: new LaunchAPI(),
userAPI: new UserAPI({ store })
})
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Since the error I'm seeing is the result of the context's user being null, I would like to step through the context() method to inspect the headers and see whether it contains a valid (encoded) email; as you can see I've set a debugger breakpoint at the start of that function.
However, when I run node inspect src/index.js, I immediately enter the debugger for an IntrospectionQuery:
If I press the 'Resume script execution' button, I just get back immediately to the same breakpoint.
Does this have something to do with the fact that context() is an async function? How can I step through the context() method with the debugger?
In the end, I just debugged it using console.log() commands. I had accidentally put the authorization in the 'Query Variables' section rather than the 'HTTP Headers' one. Moving it to 'HTTP Headers' produces the desired result: