Skip to content

Commit 13040ab

Browse files
committed
server: begin implementing header auth
1 parent 6a951ca commit 13040ab

File tree

13 files changed

+170
-57
lines changed

13 files changed

+170
-57
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ You can change these to your liking.
5555
- `ENABLE_ADMIN`: the first account created is an administrator account
5656
- `DRIFT_HOME`: defaults to ~/.drift, the directory for storing the database and eventually images
5757

58+
### For SSO
59+
60+
- `HEADER_AUTH`: if true, enables authenthication via the HTTP header specified in `HEADER_AUTH_KEY` which generally populated at the reverse-proxy level.
61+
- `HEADER_AUTH_KEY`: if `HEADER_AUTH` is true, the header to look for the users username (like `Auth-User`)
62+
- `HEADER_AUTH_ROLE`: if `HEADER_AUTH` is true, the header to look for the users role ("user" | "admin", at the moment)
63+
5864
## Running with pm2
5965

6066
It's easy to start Drift using [pm2](https://pm2.keymetrics.io/).

client/lib/hooks/use-signed-in.ts

+23
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Cookies from "js-cookie"
22
import { useEffect } from "react"
33
import useSharedState from "./use-shared-state"
44

5+
56
const useSignedIn = () => {
67
const [signedIn, setSignedIn] = useSharedState(
78
"signedIn",
@@ -14,6 +15,28 @@ const useSignedIn = () => {
1415
Cookies.set("drift-token", token)
1516
}
1617

18+
useEffect(() => {
19+
const attemptSignIn = async () => {
20+
// If header auth is enabled, the reverse proxy will add it between this fetch and the server.
21+
// Otherwise, the token will be used.
22+
const res = await fetch("/server-api/auth/verify-token", {
23+
method: "GET",
24+
headers: {
25+
"Content-Type": "application/json",
26+
"Authorization": `Bearer ${token}`
27+
}
28+
})
29+
30+
if (res.status !== 200) {
31+
setSignedIn(false)
32+
return
33+
}
34+
}
35+
36+
attemptSignIn()
37+
}, [setSignedIn, token])
38+
39+
1740
useEffect(() => {
1841
if (token) {
1942
setSignedIn(true)

server/src/lib/config.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ type Config = {
99
registration_password: string
1010
welcome_content: string | undefined
1111
welcome_title: string | undefined
12+
header_auth: boolean
13+
header_auth_name: string | undefined
14+
header_auth_role: string | undefined
1215
}
1316

1417
type EnvironmentValue = string | undefined
@@ -78,7 +81,10 @@ export const config = (env: Environment): Config => {
7881
secret_key: developmentDefault(env.SECRET_KEY, "SECRET_KEY", "secret"),
7982
registration_password: env.REGISTRATION_PASSWORD ?? "",
8083
welcome_content: env.WELCOME_CONTENT,
81-
welcome_title: env.WELCOME_TITLE
84+
welcome_title: env.WELCOME_TITLE,
85+
header_auth: stringToBoolean(env.HEADER_AUTH),
86+
header_auth_name: env.HEADER_AUTH_NAME,
87+
header_auth_role: env.HEADER_AUTH_ROLE
8288
}
8389
return config
8490
}

server/src/lib/middleware/__tests__/is-admin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// import { app } from '../../../app'
33
import { NextFunction, Response } from "express"
44
import isAdmin from "@lib/middleware/is-admin"
5-
import { UserJwtRequest } from "@lib/middleware/jwt"
5+
import { UserJwtRequest } from "@lib/middleware/is-signed-in"
66

77
describe("is-admin middlware", () => {
88
let mockRequest: Partial<UserJwtRequest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
2+
import { NextFunction, Response } from "express"
3+
4+
describe("jwt is-signed-in middlware", () => {
5+
let mockRequest: Partial<UserJwtRequest>
6+
let mockResponse: Partial<Response>
7+
let nextFunction: NextFunction = jest.fn()
8+
9+
beforeEach(() => {
10+
mockRequest = {}
11+
mockResponse = {
12+
sendStatus: jest.fn().mockReturnThis()
13+
}
14+
})
15+
16+
it("should return 401 if no authorization header", () => {
17+
const res = mockResponse as Response
18+
jwt(mockRequest as UserJwtRequest, res, nextFunction)
19+
expect(res.sendStatus).toHaveBeenCalledWith(401)
20+
})
21+
22+
it("should return 401 if no token is supplied", () => {
23+
const req = mockRequest as UserJwtRequest
24+
req.headers = {
25+
authorization: "Bearer"
26+
}
27+
jwt(req, mockResponse as Response, nextFunction)
28+
expect(mockResponse.sendStatus).toBeCalledWith(401)
29+
})
30+
31+
// it("should return 401 if token is deleted", async () => {
32+
// try {
33+
// const tokenString = "123"
34+
35+
// const req = mockRequest as UserJwtRequest
36+
// req.headers = {
37+
// authorization: `Bearer ${tokenString}`
38+
// }
39+
// jwt(req, mockResponse as Response, nextFunction)
40+
// expect(mockResponse.sendStatus).toBeCalledWith(401)
41+
// expect(mockResponse.json).toBeCalledWith({
42+
// message: "Token is no longer valid"
43+
// })
44+
// } catch (e) {
45+
// console.log(e)
46+
// }
47+
// })
48+
})

server/src/lib/middleware/__tests__/jwt.ts

-48
This file was deleted.

server/src/lib/middleware/__tests__/secret-key.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// import * as request from 'supertest'
22
// import { app } from '../../../app'
33
import { NextFunction, Response } from "express"
4-
import { UserJwtRequest } from "@lib/middleware/jwt"
4+
import { UserJwtRequest } from "@lib/middleware/is-signed-in"
55
import secretKey from "@lib/middleware/secret-key"
66
import config from "@lib/config"
77

server/src/lib/middleware/jwt.ts renamed to server/src/lib/middleware/is-signed-in.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,33 @@ export default async function authenticateToken(
2020
const authHeader = req.headers ? req.headers["authorization"] : undefined
2121
const token = authHeader && authHeader.split(" ")[1]
2222

23+
if (config.header_auth && config.header_auth_name) {
24+
// with header auth, we assume the user is authenticated,
25+
// but their user may not be created in the database yet.
26+
27+
let user = await UserModel.findByPk(req.user?.id)
28+
if (!user) {
29+
const username = req.header[config.header_auth_name]
30+
const role = config.header_auth_role ? req.header[config.header_auth_role] || "user" : "user"
31+
user = new UserModel({
32+
username,
33+
role
34+
})
35+
await user.save()
36+
}
37+
38+
if (!token) {
39+
const token = jwt.sign({ id: user.id }, config.jwt_secret, {
40+
expiresIn: "2d"
41+
})
42+
const authToken = new AuthToken({
43+
userId: user.id,
44+
token: token
45+
})
46+
await authToken.save()
47+
}
48+
}
49+
2350
if (token == null) return res.sendStatus(401)
2451

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

3663
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
37-
if (err) return res.sendStatus(403)
64+
if (err) {
65+
if (config.header_auth) {
66+
// if the token has expired or is invalid, we need to delete it and generate a new one
67+
authToken.destroy()
68+
const token = jwt.sign({ id: user.id }, config.jwt_secret, {
69+
expiresIn: "2d"
70+
})
71+
const newToken = new AuthToken({
72+
userId: user.id,
73+
token: token
74+
})
75+
await newToken.save()
76+
} else {
77+
return res.sendStatus(403)
78+
}
79+
}
80+
3881
const userObj = await UserModel.findByPk(user.id, {
3982
attributes: {
4083
exclude: ["password"]

server/src/routes/auth.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { User } from "@lib/models/User"
44
import { AuthToken } from "@lib/models/AuthToken"
55
import { sign, verify } from "jsonwebtoken"
66
import config from "@lib/config"
7-
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
7+
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
88
import { celebrate, Joi } from "celebrate"
99
import secretKey from "@lib/middleware/secret-key"
1010

@@ -94,7 +94,11 @@ auth.post(
9494
serverPassword: Joi.string().required().allow("", null)
9595
}
9696
}),
97-
async (req, res, next) => {
97+
async (req, res) => {
98+
if (config.header_auth) {
99+
100+
}
101+
98102
const error = "User does not exist or password is incorrect"
99103
const errorToThrow = new Error(error)
100104
try {

server/src/routes/files.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { celebrate, Joi } from "celebrate"
22
import { Router } from "express"
33
import { File } from "@lib/models/File"
44
import secretKey from "@lib/middleware/secret-key"
5-
import jwt from "@lib/middleware/jwt"
5+
import jwt from "@lib/middleware/is-signed-in"
66
import getHtmlFromFile from "@lib/get-html-from-drift-file"
77

88
export const files = Router()

server/src/routes/posts.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Router } from "express"
22
import { celebrate, Joi } from "celebrate"
33
import { File } from "@lib/models/File"
44
import { Post } from "@lib/models/Post"
5-
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
5+
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
66
import * as crypto from "crypto"
77
import { User } from "@lib/models/User"
88
import secretKey from "@lib/middleware/secret-key"

server/src/routes/user.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Router } from "express"
2-
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
2+
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
33
import { User } from "@lib/models/User"
44
import { celebrate, Joi } from "celebrate"
55

server/src/routes/users.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Router } from "express"
2+
import jwt, { UserJwtRequest } from "@lib/middleware/is-signed-in"
3+
import { User } from "@lib/models/User"
4+
5+
export const users = Router()
6+
7+
users.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
8+
const error = () =>
9+
res.status(401).json({
10+
message: "Unauthorized"
11+
})
12+
13+
try {
14+
if (!req.user) {
15+
return error()
16+
}
17+
18+
const user = await User.findByPk(req.user?.id, {
19+
attributes: {
20+
exclude: ["password"]
21+
}
22+
})
23+
if (!user) {
24+
return error()
25+
}
26+
27+
res.json(user)
28+
} catch (error) {
29+
next(error)
30+
}
31+
})

0 commit comments

Comments
 (0)