Skip to content

Commit

Permalink
feat: add to api GET /user/payment, add AuthorizationTestContext to e…
Browse files Browse the repository at this point in the history
…ncapsulate magic.link test bypass (#1769)

* start user-payment

* add GET /user/payment

* encapsulate magic.link bypass hack
  • Loading branch information
gobengo authored Aug 19, 2022
1 parent 78f0c71 commit 7722acc
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 6 deletions.
7 changes: 4 additions & 3 deletions packages/api/src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
UserNotFoundError
} from './errors.js'
import { USER_TAGS } from './constants.js'
import { magicLinkBypass } from './magic.link.js'

/**
* Middleware: verify the request is authenticated with a valid magic link token.
Expand Down Expand Up @@ -143,7 +144,7 @@ export function withPinningAuthorized (handler) {
* @throws UserNotFoundError
* @returns {Promise<import('@web3-storage/db/db-client-types').UserOutput> | null }
*/
async function tryMagicToken (token, env) {
async function tryMagicToken (token, env, bypass = magicLinkBypass) {
let issuer = null
try {
env.magic.token.validate(token)
Expand All @@ -152,9 +153,9 @@ async function tryMagicToken (token, env) {
} catch (_) {
// test mode for magic admin sdk is "coming soon"
// see: https://magic.link/docs/introduction/test-mode#coming-soon
if (env.DANGEROUSLY_BYPASS_MAGIC_AUTH && token === 'test-magic') {
if (env[bypass.requiredVariableName] && token === bypass.requiredTokenValue) {
console.log(`!!! tryMagicToken bypassed with test token "${token}" !!!`)
issuer = 'test-magic-issuer'
issuer = bypass.defaults.issuer
} else {
// not a magic token, give up.
return null
Expand Down
5 changes: 3 additions & 2 deletions packages/api/src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Cluster } from '@nftstorage/ipfs-cluster'
import { DEFAULT_MODE } from './maintenance.js'
import { Logging } from './utils/logs.js'
import pkg from '../package.json'
import { defaultBypassMagicLinkVariableName } from './magic.link.js'

/**
* @typedef {object} Env
Expand Down Expand Up @@ -117,9 +118,9 @@ export function envAll (req, env, ctx) {
env.magic = new Magic(env.MAGIC_SECRET_KEY)

// We can remove this when magic admin sdk supports test mode
if (new URL(req.url).origin === 'http://testing.web3.storage' && env.DANGEROUSLY_BYPASS_MAGIC_AUTH !== 'undefined') {
if (new URL(req.url).origin === 'http://testing.web3.storage' && env[defaultBypassMagicLinkVariableName] !== 'undefined') {
// only set this in test/scripts/worker-globals.js
console.log(`!!! DANGEROUSLY_BYPASS_MAGIC_AUTH=${env.DANGEROUSLY_BYPASS_MAGIC_AUTH} !!!`)
console.log(`!!! ${defaultBypassMagicLinkVariableName}=${env[defaultBypassMagicLinkVariableName]} !!!`)
}

env.db = new DBClient({
Expand Down
16 changes: 15 additions & 1 deletion packages/api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,20 @@ import { envAll } from './env.js'
import { statusGet } from './status.js'
import { carHead, carGet, carPut, carPost } from './car.js'
import { uploadPost } from './upload.js'
import { userLoginPost, userTokensPost, userTokensGet, userTokensDelete, userUploadsGet, userUploadsDelete, userAccountGet, userUploadsRename, userInfoGet, userRequestPost, userPinsGet } from './user.js'
import {
userAccountGet,
userInfoGet,
userLoginPost,
userPaymentGet,
userPinsGet,
userRequestPost,
userTokensDelete,
userTokensGet,
userTokensPost,
userUploadsDelete,
userUploadsGet,
userUploadsRename
} from './user.js'
import { pinDelete, pinGet, pinPost, pinsGet } from './pins.js'
import { blogSubscriptionCreate } from './blog.js'
import { metricsGet } from './metrics.js'
Expand Down Expand Up @@ -98,6 +111,7 @@ router.delete('/user/tokens/:id', auth['👤🗑️'](userTokensDelete))
router.get('/user/account', auth['👤'](userAccountGet))
router.get('/user/info', auth['👤'](userInfoGet))
router.get('/user/pins', auth['📌⚠️'](userPinsGet))
router.get('/user/payment', auth['👤'](userPaymentGet))

/* eslint-enable no-multi-spaces */

Expand Down
25 changes: 25 additions & 0 deletions packages/api/src/magic.link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const createMagicTestTokenBypass = (
requiredVariableName,
requiredTokenValue
) => {
return {
requiredVariableName,
requiredTokenValue,
defaults: {
issuer: 'test-magic-issuer'
},
summary: [
'This is a bypass for testing our APIs even though most of them require a valid magic token.',
'When testing, we\'ll use a special-use token.',
'And our token-validating middleware will allow that token,',
`but *only* when env.${requiredVariableName} is truthy.`
].join(' ')
}
}

export const defaultBypassMagicLinkVariableName = 'DANGEROUSLY_BYPASS_MAGIC_AUTH'

export const magicLinkBypass = createMagicTestTokenBypass(
defaultBypassMagicLinkVariableName,
'test-magic'
)
13 changes: 13 additions & 0 deletions packages/api/src/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,16 @@ const notifySlack = async (
})
})
}

/**
* Get a user's payment settings.
*
* @param {AuthenticatedRequest} request
* @param {import('./env').Env} env
*/
export async function userPaymentGet (request, env) {
const userPaymentGetResponse = {
method: null
}
return new JSONResponse(userPaymentGetResponse)
}
30 changes: 30 additions & 0 deletions packages/api/test/contexts/authorization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as magic from '../../src/magic.link.js'

const symbol = Symbol.for('AuthorizationTestContext')

export class AuthorizationTestContext {
static install (testContext, constructorArgs = []) {
const authzContext = new AuthorizationTestContext(...constructorArgs)
testContext[symbol] = authzContext
}

static use (testContext) {
if (!(symbol in testContext)) {
throw new Error('cant use AuthorizationTestContext because it hasnt been installed yet')
}
return testContext[symbol]
}

constructor (
bypass = magic.magicLinkBypass
) {
this.bypass = bypass
}

/**
* Create a bearer token that can be used by tests that require one to test something behind basic is-user checks
*/
createUserToken () {
return this.bypass.requiredTokenValue
}
}
2 changes: 2 additions & 0 deletions packages/api/test/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import execa from 'execa'
import delay from 'delay'
import { webcrypto } from 'crypto'
import * as workerGlobals from './scripts/worker-globals.js'
import { AuthorizationTestContext } from './contexts/authorization.js'

global.crypto = webcrypto

Expand All @@ -29,6 +30,7 @@ export const mochaHooks = () => {
return {
async beforeAll () {
this.timeout(120_000)
AuthorizationTestContext.install(this)

console.log('⚡️ Starting Miniflare')
srv = await new Miniflare({
Expand Down
51 changes: 51 additions & 0 deletions packages/api/test/user-payment.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-env mocha */
import assert from 'assert'
import fetch, { Request } from '@web-std/fetch'
import { endpoint } from './scripts/constants.js'
import { AuthorizationTestContext } from './contexts/authorization.js'

function createBearerAuthorization (bearerToken) {
return `Bearer ${bearerToken}`
}

function createUserPaymentRequest (arg) {
const { path, baseUrl, authorization } = {
authorization: undefined,
path: '/user/payment',
baseUrl: endpoint,
accept: 'application/json',
method: 'get',
...arg
}
return new Request(
new URL(path, baseUrl),
{
headers: {
accept: 'application/json',
authorization
}
}
)
}

describe('GET /user/payment', () => {
it('error if no auth header', async () => {
const res = await fetch(createUserPaymentRequest())
assert(!res.ok)
})
it('error if bad auth header', async () => {
const createRandomString = () => Math.random().toString().slice(2)
const authorization = createBearerAuthorization(createRandomString())
const res = await fetch(createUserPaymentRequest({ authorization }))
assert(!res.ok)
})
it('retrieves user account data', async function () {
const token = AuthorizationTestContext.use(this).createUserToken()
const authorization = createBearerAuthorization(token)
const res = await fetch(createUserPaymentRequest({ authorization }))
assert(res.ok)
const userPaymentSettings = await res.json()
assert.equal(typeof userPaymentSettings, 'object')
assert.ok(!userPaymentSettings.method, 'userPaymentSettings.method is falsy')
})
})

0 comments on commit 7722acc

Please sign in to comment.