diff --git a/packages/api/src/auth.js b/packages/api/src/auth.js index 36982844cf..ea44538178 100644 --- a/packages/api/src/auth.js +++ b/packages/api/src/auth.js @@ -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. @@ -143,7 +144,7 @@ export function withPinningAuthorized (handler) { * @throws UserNotFoundError * @returns {Promise | null } */ -async function tryMagicToken (token, env) { +async function tryMagicToken (token, env, bypass = magicLinkBypass) { let issuer = null try { env.magic.token.validate(token) @@ -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 diff --git a/packages/api/src/env.js b/packages/api/src/env.js index 194deded5e..4b046e8a85 100644 --- a/packages/api/src/env.js +++ b/packages/api/src/env.js @@ -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 @@ -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({ diff --git a/packages/api/src/index.js b/packages/api/src/index.js index 18027eda5c..3990a7c781 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -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' @@ -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 */ diff --git a/packages/api/src/magic.link.js b/packages/api/src/magic.link.js new file mode 100644 index 0000000000..76a33e8e02 --- /dev/null +++ b/packages/api/src/magic.link.js @@ -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' +) diff --git a/packages/api/src/user.js b/packages/api/src/user.js index 51c8119a65..94ca6af5c1 100644 --- a/packages/api/src/user.js +++ b/packages/api/src/user.js @@ -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) +} diff --git a/packages/api/test/contexts/authorization.js b/packages/api/test/contexts/authorization.js new file mode 100644 index 0000000000..3a945c6144 --- /dev/null +++ b/packages/api/test/contexts/authorization.js @@ -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 + } +} diff --git a/packages/api/test/hooks.js b/packages/api/test/hooks.js index 27868602ce..65b7d33fb6 100644 --- a/packages/api/test/hooks.js +++ b/packages/api/test/hooks.js @@ -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 @@ -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({ diff --git a/packages/api/test/user-payment.spec.js b/packages/api/test/user-payment.spec.js new file mode 100644 index 0000000000..c6a4f161fa --- /dev/null +++ b/packages/api/test/user-payment.spec.js @@ -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') + }) +})