1
0
Fork 0

feat(backend): Be more strict about validating tokens accros backends

This commit is contained in:
Joost De Cock 2023-08-18 17:26:23 +02:00
parent 25ebe0b0db
commit 2f64be21d6
8 changed files with 36 additions and 22 deletions

View file

@ -10,6 +10,7 @@ datasource db {
model Apikey { model Apikey {
id String @id @default(uuid()) id String @id @default(uuid())
aud String @default("")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
expiresAt DateTime expiresAt DateTime
name String @default("") name String @default("")

View file

@ -50,6 +50,8 @@ const baseConfig = {
env: process.env.NODE_ENV || 'development', env: process.env.NODE_ENV || 'development',
// Maintainer contact // Maintainer contact
maintainer: 'joost@freesewing.org', maintainer: 'joost@freesewing.org',
// Instance
instance: process.env.BACKEND_INSTANCE || Date.now(),
// Feature flags // Feature flags
use: { use: {
github: envToBool(process.env.BACKEND_ENABLE_GITHUB), github: envToBool(process.env.BACKEND_ENABLE_GITHUB),
@ -110,8 +112,7 @@ const baseConfig = {
}, },
jwt: { jwt: {
secretOrKey: encryptionKey, secretOrKey: encryptionKey,
issuer: process.env.BACKEND_JWT_ISSUER || 'freesewing.org', issuer: api,
audience: process.env.BACKEND_JWT_ISSUER || 'freesewing.org',
expiresIn: process.env.BACKEND_JWT_EXPIRY || '7d', expiresIn: process.env.BACKEND_JWT_EXPIRY || '7d',
}, },
languages, languages,
@ -232,6 +233,7 @@ export const cloudflareImages = config.cloudflareImages || {}
export const forwardmx = config.forwardmx || {} export const forwardmx = config.forwardmx || {}
export const website = config.website export const website = config.website
export const githubToken = config.github.token export const githubToken = config.github.token
export const instance = config.instance
const vars = { const vars = {
BACKEND_DB_URL: ['required', 'db.url'], BACKEND_DB_URL: ['required', 'db.url'],

View file

@ -50,9 +50,9 @@ FlowsController.prototype.removeImage = async (req, res, tools) => {
* Creates a pull request for a new showcase post * Creates a pull request for a new showcase post
* See: https://freesewing.dev/reference/backend/api * See: https://freesewing.dev/reference/backend/api
*/ */
FlowsController.prototype.createShowcasePr = async (req, res, tools) => { FlowsController.prototype.createPostPr = async (req, res, tools, type) => {
const Flow = new FlowModel(tools) const Flow = new FlowModel(tools)
await Flow.createShowcasePr(req) await Flow.createPostPr(req, type)
return Flow.sendResponse(res) return Flow.sendResponse(res)
} }

View file

@ -3,6 +3,7 @@ import http from 'passport-http'
import jwt from 'passport-jwt' import jwt from 'passport-jwt'
import { ApikeyModel } from './models/apikey.mjs' import { ApikeyModel } from './models/apikey.mjs'
import { UserModel } from './models/user.mjs' import { UserModel } from './models/user.mjs'
import { api, instance } from './config.mjs'
/* /*
* In v2 we ended up with a bug where we did not properly track the last login * In v2 we ended up with a bug where we did not properly track the last login
@ -10,8 +11,14 @@ import { UserModel } from './models/user.mjs'
* this field. It's a bit of a perf hit to write to the database on ever API call * this field. It's a bit of a perf hit to write to the database on ever API call
* but it's worth it to actually know which accounts are used and which are not. * but it's worth it to actually know which accounts are used and which are not.
*/ */
async function checkAccess(uid, tools, type) { async function checkAccess(payload, tools, type) {
/*
* Don't allow tokens/keys to be used on different instances,
* even with the same encryption key
*/
if (payload.aud !== `${api}/${instance}`) return false
const User = new UserModel(tools) const User = new UserModel(tools)
const uid = payload.userId || payload._id
const ok = await User.papersPlease(uid, type) const ok = await User.papersPlease(uid, type)
return ok return ok
@ -29,7 +36,7 @@ function loadPassportMiddleware(passport, tools) {
/* /*
* We check more than merely the API key * We check more than merely the API key
*/ */
const ok = Apikey.verified ? await checkAccess(Apikey.record.userId, tools, 'key') : false const ok = Apikey.verified ? await checkAccess(Apikey.record, tools, 'key') : false
return ok return ok
? done(null, { ? done(null, {
@ -50,7 +57,7 @@ function loadPassportMiddleware(passport, tools) {
/* /*
* We check more than merely the token * We check more than merely the token
*/ */
const ok = await checkAccess(jwt_payload._id, tools, 'jwt') const ok = await checkAccess(jwt_payload, tools, 'jwt')
return ok return ok
? done(null, { ? done(null, {

View file

@ -261,6 +261,7 @@ ApikeyModel.prototype.create = async function ({ body, user }) {
try { try {
this.record = await this.prisma.apikey.create({ this.record = await this.prisma.apikey.create({
data: this.cloak({ data: this.cloak({
aud: `${this.config.api}/${this.config.instance}`,
expiresAt, expiresAt,
name: body.name, name: body.name,
level: body.level, level: body.level,

View file

@ -261,13 +261,14 @@ to English prior to merging.
` `
/* /*
* Create a (GitHub) pull request for a new showcase post * Create a (GitHub) pull request for a new blog or showcase post
* *
* @param {body} object - The request body * @param {body} object - The request body
* @param {user} object - The user as loaded by auth middleware * @param {user} object - The user as loaded by auth middleware
* @param {type} string - One of blog or showcase
* @returns {FlowModel} object - The FlowModel * @returns {FlowModel} object - The FlowModel
*/ */
FlowModel.prototype.createShowcasePr = async function ({ body, user }) { FlowModel.prototype.createPostPr = async function ({ body, user }, type) {
/* /*
* Is markdown set? * Is markdown set?
*/ */
@ -283,16 +284,16 @@ FlowModel.prototype.createShowcasePr = async function ({ body, user }) {
/* /*
* Create a new feature branch for this * Create a new feature branch for this
*/ */
const branchName = `showcase-${body.slug}` const branchName = `${type}-${body.slug}`
const branch = await createBranch({ name: branchName }) const branch = await createBranch({ name: branchName })
/* /*
* Create the file * Create the file
*/ */
const file = await createFile({ const file = await createFile({
path: `markdown/org/showcase/${body.slug}/en.md`, path: `markdown/org/${type}/${body.slug}/en.md`,
body: { body: {
message: `feat: New showcase post ${body.slug} by ${this.User.record.username}${ message: `feat: New ${type} post ${body.slug} by ${this.User.record.username}${
body.language !== 'en' ? nonEnWarning : '' body.language !== 'en' ? nonEnWarning : ''
}`, }`,
content: new Buffer.from(body.markdown).toString('base64'), content: new Buffer.from(body.markdown).toString('base64'),
@ -308,8 +309,8 @@ FlowModel.prototype.createShowcasePr = async function ({ body, user }) {
* New create the pull request * New create the pull request
*/ */
const pr = await createPullRequest({ const pr = await createPullRequest({
title: `feat: New showcase post ${body.slug} by ${this.User.record.username}`, title: `feat: New ${type} post ${body.slug} by ${this.User.record.username}`,
body: `Hey @joostdecock you should check out this awesome showcase post.${ body: `Paging @joostdecock to check out this proposed ${type} post.${
body.language !== 'en' ? nonEnWarning : '' body.language !== 'en' ? nonEnWarning : ''
}`, }`,
from: branchName, from: branchName,

View file

@ -1281,7 +1281,7 @@ UserModel.prototype.getToken = function () {
username: this.record.username, username: this.record.username,
role: this.record.role, role: this.record.role,
status: this.record.status, status: this.record.status,
aud: this.config.jwt.audience, aud: `${this.config.api}/${this.config.instance}`,
iss: this.config.jwt.issuer, iss: this.config.jwt.issuer,
}, },
this.config.jwt.secretOrKey, this.config.jwt.secretOrKey,

View file

@ -39,13 +39,15 @@ export function flowsRoutes(tools) {
Flow.removeImage(req, res, tools) Flow.removeImage(req, res, tools)
) )
// Submit a pull request for a new showcase // Submit a pull request for a new showcase or blog post
app.post('/flows/pr/showcase/jwt', passport.authenticate(...jwt), (req, res) => for (const type of ['blog', 'showcase']) {
Flow.createShowcasePr(req, res, tools) app.post(`/flows/pr/${type}/jwt`, passport.authenticate(...jwt), (req, res) =>
) Flow.createPostPr(req, res, tools, type)
app.post('/flows/pr/showcase/key', passport.authenticate(...bsc), (req, res) => )
Flow.createShowcasePr(req, res, tools) app.post(`/flows/pr/${type}/key`, passport.authenticate(...bsc), (req, res) =>
) Flow.createPostPr(req, res, tools, type)
)
}
// Create Issue - No auth needed // Create Issue - No auth needed
app.post('/issues', (req, res) => Flow.createIssue(req, res, tools)) app.post('/issues', (req, res) => Flow.createIssue(req, res, tools))