Compare commits
5 commits
refactor
...
headerAuth
Author | SHA1 | Date | |
---|---|---|---|
|
a30425a069 | ||
|
743ca20470 | ||
|
f74f7b1f1a | ||
|
05cc23a144 | ||
|
13040ab8cc |
14 changed files with 196 additions and 108 deletions
|
@ -55,6 +55,12 @@ You can change these to your liking.
|
||||||
- `ENABLE_ADMIN`: the first account created is an administrator account
|
- `ENABLE_ADMIN`: the first account created is an administrator account
|
||||||
- `DRIFT_HOME`: defaults to ~/.drift, the directory for storing the database and eventually images
|
- `DRIFT_HOME`: defaults to ~/.drift, the directory for storing the database and eventually images
|
||||||
|
|
||||||
|
### For SSO
|
||||||
|
|
||||||
|
- `HEADER_AUTH`: if true, enables authenthication via the HTTP header specified in `HEADER_AUTH_KEY` which is generally populated at the reverse-proxy level.
|
||||||
|
- `HEADER_AUTH_KEY`: if `HEADER_AUTH` is true, the header to look for the users username (like `Auth-User`)
|
||||||
|
- `HEADER_AUTH_ROLE`: if `HEADER_AUTH` is true, the header to look for the users role ("user" | "admin", at the moment)
|
||||||
|
- `HEADER_AUTH_WHITELISTED_IPS`: comma-separated list of IPs users can access Drift from using header authentication. Defaults to '127.0.0.1'.
|
||||||
## Running with pm2
|
## Running with pm2
|
||||||
|
|
||||||
It's easy to start Drift using [pm2](https://pm2.keymetrics.io/).
|
It's easy to start Drift using [pm2](https://pm2.keymetrics.io/).
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Cookies from "js-cookie"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import useSharedState from "./use-shared-state"
|
import useSharedState from "./use-shared-state"
|
||||||
|
|
||||||
|
|
||||||
const useSignedIn = () => {
|
const useSignedIn = () => {
|
||||||
const [signedIn, setSignedIn] = useSharedState(
|
const [signedIn, setSignedIn] = useSharedState(
|
||||||
"signedIn",
|
"signedIn",
|
||||||
|
@ -14,6 +15,28 @@ const useSignedIn = () => {
|
||||||
Cookies.set("drift-token", token)
|
Cookies.set("drift-token", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const attemptSignIn = async () => {
|
||||||
|
// If header auth is enabled, the reverse proxy will add it between this fetch and the server.
|
||||||
|
// Otherwise, the token will be used.
|
||||||
|
const res = await fetch("/server-api/auth/verify-signed-in", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
setSignedIn(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptSignIn()
|
||||||
|
}, [setSignedIn, token])
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
if (token) {
|
||||||
setSignedIn(true)
|
setSignedIn(true)
|
||||||
|
|
|
@ -9,6 +9,12 @@ type Config = {
|
||||||
registration_password: string
|
registration_password: string
|
||||||
welcome_content: string | undefined
|
welcome_content: string | undefined
|
||||||
welcome_title: string | undefined
|
welcome_title: string | undefined
|
||||||
|
|
||||||
|
// header auth
|
||||||
|
header_auth: boolean
|
||||||
|
header_auth_name: string | undefined
|
||||||
|
header_auth_role: string | undefined
|
||||||
|
header_auth_whitelisted_ips: string[] | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnvironmentValue = string | undefined
|
type EnvironmentValue = string | undefined
|
||||||
|
@ -55,6 +61,14 @@ export const config = (env: Environment): Config => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseArrayFromString = (str: EnvironmentValue): string[] => {
|
||||||
|
if (str) {
|
||||||
|
return str.split(",").map((s) => s.trim())
|
||||||
|
} else {
|
||||||
|
return ['127.0.0.1']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const is_production = env.NODE_ENV === "production"
|
const is_production = env.NODE_ENV === "production"
|
||||||
|
|
||||||
const developmentDefault = (
|
const developmentDefault = (
|
||||||
|
@ -78,7 +92,11 @@ export const config = (env: Environment): Config => {
|
||||||
secret_key: developmentDefault(env.SECRET_KEY, "SECRET_KEY", "secret"),
|
secret_key: developmentDefault(env.SECRET_KEY, "SECRET_KEY", "secret"),
|
||||||
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
||||||
welcome_content: env.WELCOME_CONTENT,
|
welcome_content: env.WELCOME_CONTENT,
|
||||||
welcome_title: env.WELCOME_TITLE
|
welcome_title: env.WELCOME_TITLE,
|
||||||
|
header_auth: stringToBoolean(env.HEADER_AUTH),
|
||||||
|
header_auth_name: env.HEADER_AUTH_NAME,
|
||||||
|
header_auth_role: env.HEADER_AUTH_ROLE,
|
||||||
|
header_auth_whitelisted_ips: parseArrayFromString(env.HEADER_AUTH_WHITELISTED_IPS)
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// import { app } from '../../../app'
|
// import { app } from '../../../app'
|
||||||
import { NextFunction, Response } from "express"
|
import { NextFunction, Response } from "express"
|
||||||
import isAdmin from "@lib/middleware/is-admin"
|
import isAdmin from "@lib/middleware/is-admin"
|
||||||
import { UserJwtRequest } from "@lib/middleware/jwt"
|
import { UserJwtRequest } from "@lib/middleware/is-signed-in"
|
||||||
|
|
||||||
describe("is-admin middlware", () => {
|
describe("is-admin middlware", () => {
|
||||||
let mockRequest: Partial<UserJwtRequest>
|
let mockRequest: Partial<UserJwtRequest>
|
||||||
|
|
48
server/src/lib/middleware/__tests__/is-signed-in.ts
Normal file
48
server/src/lib/middleware/__tests__/is-signed-in.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
|
||||||
|
import { NextFunction, Response } from "express"
|
||||||
|
|
||||||
|
describe("jwt is-signed-in middlware", () => {
|
||||||
|
let mockRequest: Partial<UserJwtRequest>
|
||||||
|
let mockResponse: Partial<Response>
|
||||||
|
let nextFunction: NextFunction = jest.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockRequest = {}
|
||||||
|
mockResponse = {
|
||||||
|
sendStatus: jest.fn().mockReturnThis()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 401 if no authorization header", () => {
|
||||||
|
const res = mockResponse as Response
|
||||||
|
jwt(mockRequest as UserJwtRequest, res, nextFunction)
|
||||||
|
expect(res.sendStatus).toHaveBeenCalledWith(401)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 401 if no token is supplied", () => {
|
||||||
|
const req = mockRequest as UserJwtRequest
|
||||||
|
req.headers = {
|
||||||
|
authorization: "Bearer"
|
||||||
|
}
|
||||||
|
jwt(req, mockResponse as Response, nextFunction)
|
||||||
|
expect(mockResponse.sendStatus).toBeCalledWith(401)
|
||||||
|
})
|
||||||
|
|
||||||
|
// it("should return 401 if token is deleted", async () => {
|
||||||
|
// try {
|
||||||
|
// const tokenString = "123"
|
||||||
|
|
||||||
|
// const req = mockRequest as UserJwtRequest
|
||||||
|
// req.headers = {
|
||||||
|
// authorization: `Bearer ${tokenString}`
|
||||||
|
// }
|
||||||
|
// jwt(req, mockResponse as Response, nextFunction)
|
||||||
|
// expect(mockResponse.sendStatus).toBeCalledWith(401)
|
||||||
|
// expect(mockResponse.json).toBeCalledWith({
|
||||||
|
// message: "Token is no longer valid"
|
||||||
|
// })
|
||||||
|
// } catch (e) {
|
||||||
|
// console.log(e)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
})
|
|
@ -1,48 +0,0 @@
|
||||||
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
|
||||||
import { NextFunction, Response } from "express"
|
|
||||||
|
|
||||||
describe("jwt middlware", () => {
|
|
||||||
let mockRequest: Partial<UserJwtRequest>
|
|
||||||
let mockResponse: Partial<Response>
|
|
||||||
let nextFunction: NextFunction = jest.fn()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockRequest = {}
|
|
||||||
mockResponse = {
|
|
||||||
sendStatus: jest.fn().mockReturnThis()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return 401 if no authorization header", () => {
|
|
||||||
const res = mockResponse as Response
|
|
||||||
jwt(mockRequest as UserJwtRequest, res, nextFunction)
|
|
||||||
expect(res.sendStatus).toHaveBeenCalledWith(401)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return 401 if no token is supplied", () => {
|
|
||||||
const req = mockRequest as UserJwtRequest
|
|
||||||
req.headers = {
|
|
||||||
authorization: "Bearer"
|
|
||||||
}
|
|
||||||
jwt(req, mockResponse as Response, nextFunction)
|
|
||||||
expect(mockResponse.sendStatus).toBeCalledWith(401)
|
|
||||||
})
|
|
||||||
|
|
||||||
// it("should return 401 if token is deleted", async () => {
|
|
||||||
// try {
|
|
||||||
// const tokenString = "123"
|
|
||||||
|
|
||||||
// const req = mockRequest as UserJwtRequest
|
|
||||||
// req.headers = {
|
|
||||||
// authorization: `Bearer ${tokenString}`
|
|
||||||
// }
|
|
||||||
// jwt(req, mockResponse as Response, nextFunction)
|
|
||||||
// expect(mockResponse.sendStatus).toBeCalledWith(401)
|
|
||||||
// expect(mockResponse.json).toBeCalledWith({
|
|
||||||
// message: "Token is no longer valid"
|
|
||||||
// })
|
|
||||||
// } catch (e) {
|
|
||||||
// console.log(e)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
})
|
|
|
@ -1,7 +1,7 @@
|
||||||
// import * as request from 'supertest'
|
// import * as request from 'supertest'
|
||||||
// import { app } from '../../../app'
|
// import { app } from '../../../app'
|
||||||
import { NextFunction, Response } from "express"
|
import { NextFunction, Response } from "express"
|
||||||
import { UserJwtRequest } from "@lib/middleware/jwt"
|
import { UserJwtRequest } from "@lib/middleware/is-signed-in"
|
||||||
import secretKey from "@lib/middleware/secret-key"
|
import secretKey from "@lib/middleware/secret-key"
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
|
|
||||||
|
|
91
server/src/lib/middleware/is-signed-in.ts
Normal file
91
server/src/lib/middleware/is-signed-in.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { AuthToken } from "@lib/models/AuthToken"
|
||||||
|
import { NextFunction, Request, Response } from "express"
|
||||||
|
import * as jwt from "jsonwebtoken"
|
||||||
|
import config from "../config"
|
||||||
|
import { User as UserModel } from "../models/User"
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserJwtRequest extends Request {
|
||||||
|
user?: User
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function isSignedIn(
|
||||||
|
req: UserJwtRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const authHeader = req.headers ? req.headers["authorization"] : undefined
|
||||||
|
const token = authHeader && authHeader.split(" ")[1]
|
||||||
|
|
||||||
|
if (config.header_auth && config.header_auth_name) {
|
||||||
|
if (!config.header_auth_whitelisted_ips?.includes(req.ip)) {
|
||||||
|
console.warn(`IP ${req.ip} is not whitelisted and tried to authenticate with header auth.`)
|
||||||
|
return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
|
||||||
|
// with header auth, we assume the user is authenticated,
|
||||||
|
// but their user may not be created in the database yet.
|
||||||
|
|
||||||
|
let user = await UserModel.findByPk(req.user?.id)
|
||||||
|
if (!user) {
|
||||||
|
const username = req.header[config.header_auth_name]
|
||||||
|
const role = config.header_auth_role ? req.header[config.header_auth_role] || "user" : "user"
|
||||||
|
user = new UserModel({
|
||||||
|
username,
|
||||||
|
role
|
||||||
|
})
|
||||||
|
await user.save()
|
||||||
|
console.log(`Created user ${username} with role ${role} via header auth.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = user
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
if (token == null) return res.sendStatus(401)
|
||||||
|
|
||||||
|
const authToken = await AuthToken.findOne({ where: { token: token } })
|
||||||
|
if (authToken == null) {
|
||||||
|
return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authToken.deletedAt) {
|
||||||
|
return res.sendStatus(401).json({
|
||||||
|
message: "Token is no longer valid"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
|
||||||
|
if (err) {
|
||||||
|
if (config.header_auth) {
|
||||||
|
// if the token has expired or is invalid, we need to delete it and generate a new one
|
||||||
|
authToken.destroy()
|
||||||
|
const token = jwt.sign({ id: user.id }, config.jwt_secret, {
|
||||||
|
expiresIn: "2d"
|
||||||
|
})
|
||||||
|
const newToken = new AuthToken({
|
||||||
|
userId: user.id,
|
||||||
|
token: token
|
||||||
|
})
|
||||||
|
await newToken.save()
|
||||||
|
} else {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userObj = await UserModel.findByPk(user.id, {
|
||||||
|
attributes: {
|
||||||
|
exclude: ["password"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!userObj) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
req.user = user
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,50 +0,0 @@
|
||||||
import { AuthToken } from "@lib/models/AuthToken"
|
|
||||||
import { NextFunction, Request, Response } from "express"
|
|
||||||
import * as jwt from "jsonwebtoken"
|
|
||||||
import config from "../config"
|
|
||||||
import { User as UserModel } from "../models/User"
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserJwtRequest extends Request {
|
|
||||||
user?: User
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function authenticateToken(
|
|
||||||
req: UserJwtRequest,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const authHeader = req.headers ? req.headers["authorization"] : undefined
|
|
||||||
const token = authHeader && authHeader.split(" ")[1]
|
|
||||||
|
|
||||||
if (token == null) return res.sendStatus(401)
|
|
||||||
|
|
||||||
const authToken = await AuthToken.findOne({ where: { token: token } })
|
|
||||||
if (authToken == null) {
|
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authToken.deletedAt) {
|
|
||||||
return res.sendStatus(401).json({
|
|
||||||
message: "Token is no longer valid"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
|
|
||||||
if (err) return res.sendStatus(403)
|
|
||||||
const userObj = await UserModel.findByPk(user.id, {
|
|
||||||
attributes: {
|
|
||||||
exclude: ["password"]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!userObj) {
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
req.user = user
|
|
||||||
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import { NextFunction, Request, Response } from "express"
|
import { NextFunction, Request, Response } from "express"
|
||||||
|
|
||||||
export default function authenticateToken(
|
export default function secretKey(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { User } from "@lib/models/User"
|
||||||
import { AuthToken } from "@lib/models/AuthToken"
|
import { AuthToken } from "@lib/models/AuthToken"
|
||||||
import { sign, verify } from "jsonwebtoken"
|
import { sign, verify } from "jsonwebtoken"
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
|
||||||
import { celebrate, Joi } from "celebrate"
|
import { celebrate, Joi } from "celebrate"
|
||||||
import secretKey from "@lib/middleware/secret-key"
|
import secretKey from "@lib/middleware/secret-key"
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ auth.post(
|
||||||
serverPassword: Joi.string().required().allow("", null)
|
serverPassword: Joi.string().required().allow("", null)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
async (req, res, next) => {
|
async (req, res) => {
|
||||||
const error = "User does not exist or password is incorrect"
|
const error = "User does not exist or password is incorrect"
|
||||||
const errorToThrow = new Error(error)
|
const errorToThrow = new Error(error)
|
||||||
try {
|
try {
|
||||||
|
@ -147,7 +147,7 @@ function generateAccessToken(user: User) {
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.get("/verify-token", jwt, async (req, res, next) => {
|
auth.get("/verify-signed-in", jwt, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: "You are authenticated"
|
message: "You are authenticated"
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { celebrate, Joi } from "celebrate"
|
||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import { File } from "@lib/models/File"
|
import { File } from "@lib/models/File"
|
||||||
import secretKey from "@lib/middleware/secret-key"
|
import secretKey from "@lib/middleware/secret-key"
|
||||||
import jwt from "@lib/middleware/jwt"
|
import jwt from "@lib/middleware/is-signed-in"
|
||||||
import getHtmlFromFile from "@lib/get-html-from-drift-file"
|
import getHtmlFromFile from "@lib/get-html-from-drift-file"
|
||||||
|
|
||||||
export const files = Router()
|
export const files = Router()
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Router } from "express"
|
||||||
import { celebrate, Joi } from "celebrate"
|
import { celebrate, Joi } from "celebrate"
|
||||||
import { File } from "@lib/models/File"
|
import { File } from "@lib/models/File"
|
||||||
import { Post } from "@lib/models/Post"
|
import { Post } from "@lib/models/Post"
|
||||||
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
import { User } from "@lib/models/User"
|
import { User } from "@lib/models/User"
|
||||||
import secretKey from "@lib/middleware/secret-key"
|
import secretKey from "@lib/middleware/secret-key"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
|
||||||
import { User } from "@lib/models/User"
|
import { User } from "@lib/models/User"
|
||||||
import { celebrate, Joi } from "celebrate"
|
import { celebrate, Joi } from "celebrate"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue