Skip to content

Commit

Permalink
feat(apps): create credential service server
Browse files Browse the repository at this point in the history
Create the credential service server. When logged in with an ng-dev token, allows for
a user to request via websocket, a temporary Github credential.
  • Loading branch information
josephperrott committed Jul 28, 2022
1 parent fa61d03 commit 5bb34bd
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 3 deletions.
29 changes: 29 additions & 0 deletions apps/credential-service/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
load("//tools:defaults.bzl", "esbuild_esm_bundle")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin", "nodejs_binary")

nodejs_binary(
name = "serve",
data = [
":bin_files",
],
entry_point = ":credential-service.js",
)

copy_to_bin(
name = "bin_files",
srcs = [
"package.json",
],
)

esbuild_esm_bundle(
name = "credential-service",
entry_point = "//apps/credential-service/lib:server.ts",
target = "node16",
visibility = [
"//apps/credential-service:__subpackages__",
],
deps = [
"//apps/credential-service/lib",
],
)
23 changes: 23 additions & 0 deletions apps/credential-service/lib/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
load("//tools:defaults.bzl", "ts_library")

exports_files([
"server.ts",
])

ts_library(
name = "lib",
srcs = glob([
"server.ts",
]),
visibility = [
"//apps/credential-service:__pkg__",
],
deps = [
"@npm//@octokit/auth-app",
"@npm//@octokit/rest",
"@npm//@types/node",
"@npm//@types/ws",
"@npm//firebase-admin",
"@npm//ws",
],
)
149 changes: 149 additions & 0 deletions apps/credential-service/lib/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env node

import {createServer, IncomingMessage} from 'http';
import {WebSocketServer, WebSocket} from 'ws';
import {Octokit} from '@octokit/rest';
import {createAppAuth} from '@octokit/auth-app';
import {Duplex} from 'stream';
import admin, {AppOptions} from 'firebase-admin';

/** The temporary access token and a convience method for revoking it. */
interface AccessTokenAndRevocation {
token: string;
revokeToken: () => void;
}

/** Regex for matching authorization header uses. */
const authorizationRegex = new RegExp(/Bearer (.*)/);
/** The length of time in ms between heartbeat checks. */
const heartBeatIntervalLength = 5000;
/** The port to bind the server to */
const PORT = process.env.PORT!;
/** The ID of the Github app used to generating tokens. */
const GITHUB_APP_ID = process.env.GITHUB_APP_ID!;
/** The PEM key of the Github app used to generating tokens. */
const GITHUB_APP_PEM = process.env.GITHUB_APP_PEM;
/** The firebase confgiuration for the firebase application being used for authentication. */
const FIREBASE_APP_CONFIG = JSON.parse(process.env.FIREBASE_APP_CONFIG!) as AppOptions;

// Initialize the Firebase application.
admin.initializeApp(FIREBASE_APP_CONFIG);

/** Generate a temporary access token with access to the requested repository. */
export async function generateAccessToken(
owner: string,
repo: string,
): Promise<AccessTokenAndRevocation> {
/** The github client used for generating the token. */
const github = new Octokit({
authStrategy: createAppAuth,
auth: {appId: GITHUB_APP_ID, privateKey: GITHUB_APP_PEM},
});
/** The specific installation id for the provided repository. */
const {id: installation_id} = (await github.apps.getRepoInstallation({owner, repo})).data;
/** A temporary github access token. */
const {token} = (await github.rest.apps.createInstallationAccessToken({installation_id})).data;

return {
token,
revokeToken: async () => await github.apps.revokeInstallationAccessToken(),
};
}

/**
* WebSocket handler to generate temporary Github access token.
*
* The access token is automatically revoked when the websocket is closed.
*/
async function wsHandler(ws: WebSocket, req: IncomingMessage) {
/** Whether the websocket heartbeat check is still alive. */
let hasHeartbeat: boolean;
/** The interval instance for checking the heartbeat. */
let heartbeatInterval = setInterval(checkHeartbeat, heartBeatIntervalLength);
/**
* The repository name and owner for the request.
* Note: We safely case the header type as having these fields because they are checked prior
* to the WebSocket handler function being invoked.
*/
const {repo, owner} = req.headers as {repo: string; owner: string};

/** The temporary Github access token and function to revoke the token.. */
const {token, revokeToken} = await generateAccessToken(owner, repo);
/** Check to make sure the heartbeat is still alive. */
function checkHeartbeat() {
if (hasHeartbeat === false) {
ws.close(1008, 'Cannot find socket via heartbeat check');
ws.terminate();
}
hasHeartbeat = false;
ws.ping();
}

/**
* Cleans up the state of the function when the WebSocket is completed.
*/
async function complete() {
await revokeToken();
clearInterval(heartbeatInterval);
}

// Ensure that cleanup is done when the WebSocket closes.
ws.on('close', complete);
ws.on('error', complete);

// Handle the pong response from the websocket client, updating the heartbeat as alive.
ws.on('pong', () => (hasHeartbeat = true));

// Send the temporary Github token to the websocket client
ws.send(token);
}

/**
* Handle upgrade requests before the websocket is used. Enforces authentication mechanisms
* and ensuring the required data is present in the request.
*/
async function upgradeHandler(req: IncomingMessage, socket: Duplex, head: Buffer) {
try {
if (!authorizationRegex.test(req.headers.authorization!)) {
throw Error('Missing or invalid authorization header syntax');
}
/** The NgDev token from the user to be verified. */
const [_, ngDevToken] = req.headers.authorization!.match(/Bearer (.*)/)!;
await admin
.auth()
.verifySessionCookie(ngDevToken, /* checkRevoked */ true)
.then((decodedToken: admin.auth.DecodedIdToken) => {
console.log(`Verified login of ${decodedToken.email}`);
});
} catch (e) {
console.error('Unable to verified authorized user');
console.error(e);
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}

/** The repository name and owner for the request. */
const {repo, owner} = req.headers as {repo: string; owner: string};

if (!repo || !owner) {
console.error('Missing a repo or owner parameter');
socket.write('HTTP/1.1 400 Bad Reqest\r\n\r\n');
socket.destroy();
return;
}

wss.handleUpgrade(req, socket, head, (ws: WebSocket, req: IncomingMessage) => {
wss.emit('connection', ws, req);
});
}

/** The http web server. */
const server = createServer();
/** The websocket server to handle websocket requests. */
const wss = new WebSocketServer({noServer: true});

wss.on('connection', wsHandler);
server.on('upgrade', upgradeHandler);
server.on('listening', () => console.log('Credential Service startup complete, listening'));
server.listen(parseInt(PORT, 10));
3 changes: 3 additions & 0 deletions apps/credential-service/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@types/send": "^0.17.1",
"@types/tmp": "^0.2.1",
"@types/uuid": "^8.3.1",
"@types/ws": "8.5.3",
"@types/yargs": "^17.0.0",
"browser-sync": "^2.27.7",
"clang-format": "1.8.0",
Expand Down Expand Up @@ -85,6 +86,8 @@
"@types/conventional-commits-parser": "^3.0.1",
"@types/ejs": "^3.0.6",
"@types/events": "^3.0.0",
"@types/express": "^4.17.13",
"@types/express-ws": "^3.0.1",
"@types/folder-hash": "^4.0.1",
"@types/git-raw-commits": "^2.0.0",
"@types/glob": "^7.1.3",
Expand All @@ -102,6 +105,8 @@
"cli-progress": "^3.7.0",
"conventional-commits-parser": "^3.2.1",
"ejs": "^3.1.6",
"express": "^4.18.1",
"express-ws": "^5.0.2",
"firebase": "9.9.0",
"firebase-admin": "^11.0.0",
"firebase-functions": "^3.21.2",
Expand Down Expand Up @@ -141,6 +146,7 @@
"typed-graphqlify": "^3.1.1",
"wait-on": "^6.0.0",
"which": "^2.0.2",
"ws": "^8.8.0",
"yaml": "^2.0.0",
"zone.js": "^0.11.4"
},
Expand Down
34 changes: 31 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,8 @@ __metadata:
"@types/conventional-commits-parser": ^3.0.1
"@types/ejs": ^3.0.6
"@types/events": ^3.0.0
"@types/express": ^4.17.13
"@types/express-ws": ^3.0.1
"@types/folder-hash": ^4.0.1
"@types/git-raw-commits": ^2.0.0
"@types/glob": ^7.1.3
Expand All @@ -375,6 +377,7 @@ __metadata:
"@types/uuid": ^8.3.1
"@types/wait-on": ^5.3.1
"@types/which": ^2.0.1
"@types/ws": 8.5.3
"@types/yargs": ^17.0.0
"@types/yarnpkg__lockfile": ^1.1.5
"@yarnpkg/lockfile": ^1.1.0
Expand All @@ -384,6 +387,8 @@ __metadata:
cli-progress: ^3.7.0
conventional-commits-parser: ^3.2.1
ejs: ^3.1.6
express: ^4.18.1
express-ws: ^5.0.2
firebase: 9.9.0
firebase-admin: ^11.0.0
firebase-functions: ^3.21.2
Expand Down Expand Up @@ -433,6 +438,7 @@ __metadata:
uuid: ^8.3.2
wait-on: ^6.0.0
which: ^2.0.2
ws: ^8.8.0
yaml: ^2.0.0
yargs: ^17.0.0
zone.js: ^0.11.4
Expand Down Expand Up @@ -3998,6 +4004,17 @@ __metadata:
languageName: node
linkType: hard

"@types/express-ws@npm:^3.0.1":
version: 3.0.1
resolution: "@types/express-ws@npm:3.0.1"
dependencies:
"@types/express": "*"
"@types/express-serve-static-core": "*"
"@types/ws": "*"
checksum: fe0979f0c4ae6ce1d3cd6e9e5a99882d7dd461a774b3ad67066965db058bc9cc90ec9deed6bb4ee8414cf50b800ab84d6d042d375ee3b0b31483a3ec91647146
languageName: node
linkType: hard

"@types/express@npm:*, @types/express@npm:^4.11.1, @types/express@npm:^4.17.13":
version: 4.17.13
resolution: "@types/express@npm:4.17.13"
Expand Down Expand Up @@ -4399,7 +4416,7 @@ __metadata:
languageName: node
linkType: hard

"@types/ws@npm:*, @types/ws@npm:^8.5.1":
"@types/ws@npm:*, @types/ws@npm:8.5.3, @types/ws@npm:^8.5.1":
version: 8.5.3
resolution: "@types/ws@npm:8.5.3"
dependencies:
Expand Down Expand Up @@ -7899,7 +7916,18 @@ __metadata:
languageName: node
linkType: hard

"express@npm:^4.16.2, express@npm:^4.16.4, express@npm:^4.17.1, express@npm:^4.17.3":
"express-ws@npm:^5.0.2":
version: 5.0.2
resolution: "express-ws@npm:5.0.2"
dependencies:
ws: ^7.4.6
peerDependencies:
express: ^4.0.0 || ^5.0.0-alpha.1
checksum: a7134c51b6a630a369bbc7e06b6fad9ec174d535dd76c990ea6285e6cb08abad408ddb1162ba347ec5725fc483ae9f035f2eecb22ea91f3ecebff05772f62f0b
languageName: node
linkType: hard

"express@npm:^4.16.2, express@npm:^4.16.4, express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.1":
version: 4.18.1
resolution: "express@npm:4.18.1"
dependencies:
Expand Down Expand Up @@ -16634,7 +16662,7 @@ __metadata:
languageName: node
linkType: hard

"ws@npm:^7.2.3":
"ws@npm:^7.2.3, ws@npm:^7.4.6":
version: 7.5.8
resolution: "ws@npm:7.5.8"
peerDependencies:
Expand Down

0 comments on commit 5bb34bd

Please sign in to comment.