Skip to content

Commit

Permalink
server: begin implementing header auth
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxLeiter committed May 7, 2022
1 parent 6a951ca commit 13040ab
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 57 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ You can change these to your liking.
- `ENABLE_ADMIN`: the first account created is an administrator account
- `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 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)

## Running with pm2

It's easy to start Drift using [pm2](https://pm2.keymetrics.io/).
Expand Down
23 changes: 23 additions & 0 deletions client/lib/hooks/use-signed-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Cookies from "js-cookie"
import { useEffect } from "react"
import useSharedState from "./use-shared-state"


const useSignedIn = () => {
const [signedIn, setSignedIn] = useSharedState(
"signedIn",
Expand All @@ -14,6 +15,28 @@ const useSignedIn = () => {
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-token", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
}
})

if (res.status !== 200) {
setSignedIn(false)
return
}
}

attemptSignIn()
}, [setSignedIn, token])


useEffect(() => {
if (token) {
setSignedIn(true)
Expand Down
8 changes: 7 additions & 1 deletion server/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ type Config = {
registration_password: string
welcome_content: string | undefined
welcome_title: string | undefined
header_auth: boolean
header_auth_name: string | undefined
header_auth_role: string | undefined
}

type EnvironmentValue = string | undefined
Expand Down Expand Up @@ -78,7 +81,10 @@ export const config = (env: Environment): Config => {
secret_key: developmentDefault(env.SECRET_KEY, "SECRET_KEY", "secret"),
registration_password: env.REGISTRATION_PASSWORD ?? "",
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
}
return config
}
Expand Down
2 changes: 1 addition & 1 deletion server/src/lib/middleware/__tests__/is-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// import { app } from '../../../app'
import { NextFunction, Response } from "express"
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", () => {
let mockRequest: Partial<UserJwtRequest>
Expand Down
48 changes: 48 additions & 0 deletions server/src/lib/middleware/__tests__/is-signed-in.ts
Original file line number Diff line number Diff line change
@@ -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)
// }
// })
})
48 changes: 0 additions & 48 deletions server/src/lib/middleware/__tests__/jwt.ts

This file was deleted.

2 changes: 1 addition & 1 deletion server/src/lib/middleware/__tests__/secret-key.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// import * as request from 'supertest'
// import { app } from '../../../app'
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 config from "@lib/config"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,33 @@ export default async function authenticateToken(
const authHeader = req.headers ? req.headers["authorization"] : undefined
const token = authHeader && authHeader.split(" ")[1]

if (config.header_auth && config.header_auth_name) {
// 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()
}

if (!token) {
const token = jwt.sign({ id: user.id }, config.jwt_secret, {
expiresIn: "2d"
})
const authToken = new AuthToken({
userId: user.id,
token: token
})
await authToken.save()
}
}

if (token == null) return res.sendStatus(401)

const authToken = await AuthToken.findOne({ where: { token: token } })
Expand All @@ -34,7 +61,23 @@ export default async function authenticateToken(
}

jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
if (err) return res.sendStatus(403)
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"]
Expand Down
8 changes: 6 additions & 2 deletions server/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { User } from "@lib/models/User"
import { AuthToken } from "@lib/models/AuthToken"
import { sign, verify } from "jsonwebtoken"
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 secretKey from "@lib/middleware/secret-key"

Expand Down Expand Up @@ -94,7 +94,11 @@ auth.post(
serverPassword: Joi.string().required().allow("", null)
}
}),
async (req, res, next) => {
async (req, res) => {
if (config.header_auth) {

}

const error = "User does not exist or password is incorrect"
const errorToThrow = new Error(error)
try {
Expand Down
2 changes: 1 addition & 1 deletion server/src/routes/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { celebrate, Joi } from "celebrate"
import { Router } from "express"
import { File } from "@lib/models/File"
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"

export const files = Router()
Expand Down
2 changes: 1 addition & 1 deletion server/src/routes/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Router } from "express"
import { celebrate, Joi } from "celebrate"
import { File } from "@lib/models/File"
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 { User } from "@lib/models/User"
import secretKey from "@lib/middleware/secret-key"
Expand Down
2 changes: 1 addition & 1 deletion server/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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 { celebrate, Joi } from "celebrate"

Expand Down
31 changes: 31 additions & 0 deletions server/src/routes/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Router } from "express"
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
import { User } from "@lib/models/User"

export const users = Router()

users.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
const error = () =>
res.status(401).json({
message: "Unauthorized"
})

try {
if (!req.user) {
return error()
}

const user = await User.findByPk(req.user?.id, {
attributes: {
exclude: ["password"]
}
})
if (!user) {
return error()
}

res.json(user)
} catch (error) {
next(error)
}
})

0 comments on commit 13040ab

Please sign in to comment.