Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(authlify): get authlify token from header #326

Merged
merged 8 commits into from
Apr 4, 2022
53 changes: 53 additions & 0 deletions src/lib/onegraph_request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Buffer } from 'buffer'
import { request } from 'https'
import { env } from 'process'

const siteId = env.SITE_ID

export const oneGraphRequest = function (secretToken: string, requestBody: Uint8Array): Promise<string> {
dwwoelfel marked this conversation as resolved.
Show resolved Hide resolved
return new Promise((resolve, reject) => {
const port = 443

const options = {
host: 'serve.onegraph.com',
dwwoelfel marked this conversation as resolved.
Show resolved Hide resolved
path: `/graphql?app_id=${siteId}`,
port,
method: 'POST',
headers: {
Authorization: `Bearer ${secretToken}`,
'Content-Type': 'application/json',
Accept: 'application/json',
'Content-Length': requestBody ? Buffer.byteLength(requestBody) : 0,
},
}

const req = request(options, (res) => {
if (res.statusCode !== 200) {
return reject(new Error(String(res.statusCode)))
}

const body: Array<Uint8Array> = []

res.on('data', (chunk) => {
body.push(chunk)
})

res.on('end', () => {
const data = Buffer.concat(body).toString()
try {
resolve(data)
} catch (error) {
reject(error)
}
})
})

req.on('error', (error) => {
reject(error)
})

req.write(requestBody)

req.end()
})
}
6 changes: 3 additions & 3 deletions src/lib/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Event as HandlerEvent } from '../function/event'
import { BaseHandler, HandlerCallback } from '../function/handler'
import { Response } from '../function/response'

import { getSecrets, HandlerEventWithOneGraph, NetlifySecrets } from './secrets_helper'
import { getSecrets, NetlifySecrets } from './secrets_helper'
// Fine-grained control during the preview, less necessary with a more proactive OneGraph solution
export { getSecrets } from './secrets_helper'

Expand All @@ -17,12 +17,12 @@ export type HandlerWithSecrets = BaseHandler<Response, ContextWithSecrets>
export const withSecrets =
(handler: BaseHandler<Response, ContextWithSecrets>) =>
async (
event: HandlerEventWithOneGraph | HandlerEvent,
event: HandlerEvent,
context: HandlerContext,
// eslint-disable-next-line promise/prefer-await-to-callbacks
callback: HandlerCallback<Response>,
) => {
const secrets = await getSecrets(event as HandlerEventWithOneGraph)
const secrets = await getSecrets(event)

return handler(event, { ...context, secrets }, callback)
}
108 changes: 42 additions & 66 deletions src/lib/secrets_helper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Buffer } from 'buffer'
import { request } from 'https'
import { env } from 'process'

import { Event as HandlerEvent } from '../function/event'

import { oneGraphRequest } from './onegraph_request'

const TOKEN_HEADER = 'x-nf-graph-token'

const services = {
gitHub: null,
spotify: null,
Expand Down Expand Up @@ -50,8 +50,6 @@ type OneGraphSecretsResponse = {
}
}

const siteId = env.SITE_ID

const camelize = function (text: string) {
const safe = text.replace(/[-_\s.]+(.)?/g, (_, sub) => (sub ? sub.toUpperCase() : ''))
return safe.slice(0, 1).toLowerCase() + safe.slice(1)
Expand All @@ -69,55 +67,6 @@ const serviceNormalizeOverrides: ServiceNormalizeOverrides = {
GITHUB: 'gitHub',
}

const oneGraphRequest = function (secretToken: string, requestBody: Uint8Array): Promise<OneGraphSecretsResponse> {
return new Promise((resolve, reject) => {
const port = 443

const options = {
host: 'serve.onegraph.com',
path: `/graphql?app_id=${siteId}`,
port,
method: 'POST',
headers: {
Authorization: `Bearer ${secretToken}`,
'Content-Type': 'application/json',
Accept: 'application/json',
'Content-Length': requestBody ? Buffer.byteLength(requestBody) : 0,
},
}

const req = request(options, (res) => {
if (res.statusCode !== 200) {
return reject(new Error(String(res.statusCode)))
}

const body: Array<Uint8Array> = []

res.on('data', (chunk) => {
body.push(chunk)
})

res.on('end', () => {
const data = Buffer.concat(body).toString()
try {
const result: OneGraphSecretsResponse = JSON.parse(data)
resolve(result)
} catch (error) {
reject(error)
}
})
})

req.on('error', (error) => {
reject(error)
})

req.write(requestBody)

req.end()
})
}

const formatSecrets = (result: OneGraphSecretsResponse | undefined) => {
const responseServices = result?.data?.me?.serviceMetadata?.loggedInServices

Expand All @@ -133,20 +82,46 @@ const formatSecrets = (result: OneGraphSecretsResponse | undefined) => {
return newSecrets
}

type OneGraphPayload = { authlifyToken: string | undefined }
interface RequestHeaders {
dwwoelfel marked this conversation as resolved.
Show resolved Hide resolved
get(name: string): string | null
}

interface IncomingMessageHeaders {
dwwoelfel marked this conversation as resolved.
Show resolved Hide resolved
[key: string]: string
}

interface HasHeaders {
dwwoelfel marked this conversation as resolved.
Show resolved Hide resolved
headers: RequestHeaders | IncomingMessageHeaders
}

export type HandlerEventWithOneGraph = HandlerEvent & OneGraphPayload
const hasRequestStyleHeaders = function (headers: RequestHeaders | IncomingMessageHeaders): headers is RequestHeaders {
dwwoelfel marked this conversation as resolved.
Show resolved Hide resolved
return (headers as RequestHeaders).get !== undefined && typeof headers.get === 'function'
}

// This function accepts null for backwards compatibility with version < 0.11.1,
// where getSecrets did not require an event
const graphTokenFromEvent = function (event: HasHeaders | null): string | null {
if (!event) {
return null
}

const { headers } = event
// Check if object first in case there is a header with key `get`
if (TOKEN_HEADER in headers) {
return headers[TOKEN_HEADER]
dwwoelfel marked this conversation as resolved.
Show resolved Hide resolved
}
if (hasRequestStyleHeaders(headers)) {
return headers.get(TOKEN_HEADER)
}
return null
}

// Note: We may want to have configurable "sets" of secrets,
// e.g. "dev" and "prod"
export const getSecrets = async (
event?: HandlerEventWithOneGraph | HandlerEvent | undefined,
): Promise<NetlifySecrets> => {
// Allow us to get the token from event if present, else fallback to checking the env
const eventToken = (event as HandlerEventWithOneGraph)?.authlifyToken
const secretToken = eventToken || env.ONEGRAPH_AUTHLIFY_TOKEN

if (!secretToken) {
export const getSecrets = async (event: HandlerEvent): Promise<NetlifySecrets> => {
dwwoelfel marked this conversation as resolved.
Show resolved Hide resolved
const graphToken = graphTokenFromEvent(event as HasHeaders)

if (!graphToken) {
return {}
}

Expand Down Expand Up @@ -182,7 +157,8 @@ export const getSecrets = async (
const body = JSON.stringify({ query: doc })

// eslint-disable-next-line node/no-unsupported-features/node-builtins
const result = await oneGraphRequest(secretToken, new TextEncoder().encode(body))
const resultBody = await oneGraphRequest(graphToken, new TextEncoder().encode(body))
const result: OneGraphSecretsResponse = JSON.parse(resultBody)

const newSecrets = formatSecrets(result)

Expand Down