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

Basic authorization header #1039

Merged
merged 8 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ module.exports = async (req, res) => {
// The authentication method returns an error.
if (user && user instanceof Error) {

if (req.headers.authorization) {

return res.status(401).send(user.message)
}

// Remove cookie.
res.setHeader('Set-Cookie', `${process.env.TITLE}=null;HttpOnly;Max-Age=0;Path=${process.env.DIR || '/'};SameSite=Strict${!req.headers.host.includes('localhost') && ';Secure' || ''}`)

Expand Down
2 changes: 1 addition & 1 deletion mod/user/_user.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const methods = {
}
}

module.exports = (req, res) => {
module.exports = async (req, res) => {

const method = methods[req.params.method]

Expand Down
37 changes: 15 additions & 22 deletions mod/user/acl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,30 @@ const { Pool } = require('pg');

const connection = process.env.PRIVATE?.split('|') || process.env.PUBLIC?.split('|')

let pool = null
if(!connection || !connection[1]) return

module.exports = () => {
const acl_table = connection[1].split('.').pop()

if(!connection || !connection[1]) return
const acl_schema = connection[1].split('.')[0] === acl_table ? 'public' : connection[1].split('.')[0]

const acl_table = connection[1].split('.').pop()
const pool = new Pool({
connectionString: connection[0]
})

const acl_schema = connection[1].split('.')[0] === acl_table ? 'public' : connection[1].split('.')[0]
module.exports = async (q, arr) => {

// Create PostgreSQL connection pool for ACL table.
pool ??= new Pool({
connectionString: connection[0]
})
try {

// Method to query ACL. arr must be empty array by default.
return async (q, arr) => {
const client = await pool.connect()

try {
const { rows } = await client.query(q.replace(/acl_table/g, acl_table).replace(/acl_schema/g, acl_schema), arr)

const client = await pool.connect()
client.release()

const { rows } = await client.query(q.replace(/acl_table/g, acl_table).replace(/acl_schema/g, acl_schema), arr)
return rows

client.release()

return rows

} catch (err) {
console.error(err)
return err
}
} catch (err) {
console.error(err)
return err
}
}
2 changes: 1 addition & 1 deletion mod/user/add.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const acl = require('./acl')()
const acl = require('./acl')

module.exports = async (req, res) => {

Expand Down
9 changes: 8 additions & 1 deletion mod/user/auth.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
const jwt = require('jsonwebtoken')

const acl = require('./acl')()
const acl = require('./acl')

const fromACL = require('./fromACL')

module.exports = async (req, res) => {

if (req.headers.authorization) {

return await fromACL(req)
}

// Get token from params or cookie.
const token = req.params.token || req.cookies && req.cookies[process.env.TITLE]

Expand Down
2 changes: 1 addition & 1 deletion mod/user/delete.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const acl = require('./acl')()
const acl = require('./acl')

const mailer = require('../utils/mailer')

Expand Down
228 changes: 228 additions & 0 deletions mod/user/fromACL.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
const bcrypt = require('bcryptjs')

const crypto = require('crypto')

const mailer = require('../utils/mailer')

const languageTemplates = require('../utils/languageTemplates')

const acl = require('./acl')

const { nanoid } = require('nanoid')

module.exports = async (req) => {

const request = {
email: req.body?.email,
password: req.body?.password,
language: req.params.language,
headers: req.headers
}

if (req.headers.authorization) {

const user_string = Buffer.from(req.headers.authorization.split(" ")[1], 'base64').toString()

const email_password = user_string.split(':')

request.email = email_password[0]

request.password = email_password[1]
}

request.remote_address = /^[A-Za-z0-9.,_-\s]*$/.test(req.headers['x-forwarded-for'])
? req.headers['x-forwarded-for'] : undefined;

if (!request.email) return new Error(await languageTemplates({
template: 'missing_email',
language: request.language
}))

if (!request.password) return new Error(await languageTemplates({
template: 'missing_password',
language: request.language
}))

request.date = new Date()

// Get the host for the account verification email.
request.host = `${req.headers.origin
|| req.headers.referer && new URL(req.headers.referer).origin
|| 'https://' + (process.env.ALIAS || req.headers.host)}${process.env.DIR}`

const user = await getUser(request)

if (user instanceof Error) {

return await failedLogin(request)
}

return user
}

async function getUser(request) {

// Update access_log and return user record matched by email.
let rows = await acl(`
UPDATE acl_schema.acl_table
SET access_log = array_append(access_log, '${request.date.toISOString().replace(/\..*/, '')}@${request.remote_address}')
WHERE lower(email) = lower($1)
RETURNING email, roles, language, blocked, approved, approved_by, verified, admin, password ${process.env.APPROVAL_EXPIRY ? ', expires_on;' : ';'}`,
[request.email])

if (rows instanceof Error) return new Error(await languageTemplates({
template: 'failed_query',
language: request.language
}))

// Get user record from first row.
const user = rows[0]

if (!user) return new Error('auth_failed')

// Blocked user cannot login.
if (user.blocked) {
return new Error(await languageTemplates({
template: 'user_blocked',
language: user.language
}))
}

if (await userExpiry(user, request)) {
return new Error(await languageTemplates({
template: 'user_expired',
language: user.language
}))
}

// Accounts must be verified and approved for login
if (!user.verified || !user.approved) {

await mailer({
template: 'failed_login',
language: user.language,
to: user.email,
host: request.host,
remote_address: request.remote_address
})

return new Error('user_not_verified')
}

// Check password from post body against encrypted password from ACL.
if (bcrypt.compareSync(request.password, user.password)) {

// password must be removed after check
delete user.password

if (process.env.NANO_SESSION) {

const nano_session = nanoid()

user.session = nano_session

rows = await acl(`
UPDATE acl_schema.acl_table
SET session = '${nano_session}'
WHERE lower(email) = lower($1)`,
[request.email])

if (rows instanceof Error) return new Error(await languageTemplates({
template: 'failed_query',
language: request.language
}))

}

return user
}

return new Error('compare_sync_fail')
}

async function userExpiry(user, request) {

// Admin accounts do not not expire.
if (user.admin) return false;

// APPROVAL_EXPIRY is not configured.
if (!process.env.APPROVAL_EXPIRY) return false;

// Check whether user is expired.
if (user.expires_on !== null && user.expires_on < new Date() / 1000) {

if (user.approved) {

// Remove approval of expired user.
await acl(`
UPDATE acl_schema.acl_table
SET approved = false
WHERE lower(email) = lower($1);`,
[request.email])
}

// User approval has expired.
return true;
}
}

async function failedLogin(request) {

// Increase failed login attempts counter by 1.
let rows = await acl(`
UPDATE acl_schema.acl_table
SET failedattempts = failedattempts + 1
WHERE lower(email) = lower($1)
RETURNING failedattempts;`, [request.email])

if (rows instanceof Error) return new Error(await languageTemplates({
template: 'failed_query',
language: request.language
}))

// Check whether failed login attempts exceeds limit.
if (rows[0].failedattempts >= parseInt(process.env.FAILED_ATTEMPTS || 3)) {

// Create a verificationtoken.
const verificationtoken = crypto.randomBytes(20).toString('hex')

// Store verificationtoken and remove verification status.
rows = await acl(`
UPDATE acl_schema.acl_table
SET
verified = false,
verificationtoken = '${verificationtoken}'
WHERE lower(email) = lower($1);`, [request.email])

if (rows instanceof Error) return new Error(await languageTemplates({
template: 'failed_query',
language: request.language
}))

await mailer({
template: 'locked_account',
language: request.language,
to: request.email,
host: request.host,
failed_attempts: parseInt(process.env.FAILED_ATTEMPTS) || 3,
remote_address: request.remote_address,
verificationtoken
})

return new Error(await languageTemplates({
template: 'user_locked',
language: request.language
}))
}

// Login has failed but account is not locked (yet).
await mailer({
template: 'login_incorrect',
language: request.language,
to: request.email,
host: request.host,
remote_address: request.remote_address
})

return new Error('auth_failed')
}
7 changes: 4 additions & 3 deletions mod/user/key.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const acl = require('./acl')()
const acl = require('./acl')

const jwt = require('jsonwebtoken')

Expand All @@ -13,7 +13,9 @@ module.exports = async (req, res) => {

const user = rows[0]

if (!user || !user.api || user.api === 'false' || !user.verified || !user.approved || user.blocked) return res.status(401).send('Invalid token.')
if (!user || !user.api || !user.verified || !user.approved || user.blocked) {
return res.status(401).send('Unauthorized access.')
}

// Create signed api_token
const api_user = {
Expand All @@ -33,5 +35,4 @@ module.exports = async (req, res) => {

// Send ACL token.
res.send(key)

}
2 changes: 1 addition & 1 deletion mod/user/list.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const acl = require('./acl')()
const acl = require('./acl')

module.exports = async (req, res) => {

Expand Down
2 changes: 1 addition & 1 deletion mod/user/log.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const acl = require('./acl')()
const acl = require('./acl')

module.exports = async (req, res) => {

Expand Down
Loading