Skip to content

Commit

Permalink
feat: support auth via dash cookies (#539)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinKolarik authored Aug 21, 2024
1 parent 2ae4ea7 commit 2f33c29
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 143 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ Most IDEs have plugins integrating the used linter (eslint), including support f
- `FAKE_PROBE_IP=1` used in development to use a random fake ip assigned by the API
- `ADMIN_KEY={value}` used to access additional information over the API
- `SYSTEM_API_KEY={value}` used for integration with the dashboard
- `SERVER_SESSION_COOKIE_SECRET={value}` used to read the shared session cookie
- `DB_CONNECTION_HOST`, `DB_CONNECTION_USER`, `DB_CONNECTION_PASSWORD`, and `DB_CONNECTION_DATABASE` database connection details
4 changes: 4 additions & 0 deletions config/default.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ module.exports = {
docsHost: 'https://www.jsdelivr.com',
port: 3000,
processes: 2,
session: {
cookieName: 'dash_session_token',
cookieSecret: '',
},
},
redis: {
url: 'redis://localhost:6379',
Expand Down
5 changes: 5 additions & 0 deletions config/test.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
module.exports = {
server: {
session: {
cookieSecret: 'xxx',
},
},
redis: {
url: 'redis://localhost:16379',
socket: {
Expand Down
17 changes: 13 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"http-errors": "^2.0.0",
"ipaddr.js": "^2.2.0",
"joi": "^17.13.0",
"jose": "^5.7.0",
"knex": "^3.1.0",
"koa": "^2.15.3",
"koa-bodyparser": "^4.4.1",
Expand Down
62 changes: 43 additions & 19 deletions src/lib/http/middleware/authenticate.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,54 @@
import { jwtVerify } from 'jose';

import { auth } from '../auth.js';
import type { ExtendedMiddleware } from '../../../types.js';

export const authenticate: ExtendedMiddleware = async (ctx, next) => {
const { headers } = ctx.request;
type SessionCookiePayload = {
id?: string;
role?: string;
app_access?: number;
admin_access?: number;
};

if (headers && headers.authorization) {
const parts = headers.authorization.split(' ');
export const authenticate = (options: AuthenticateOptions): ExtendedMiddleware => {
const sessionKey = Buffer.from(options.session.cookieSecret);

if (parts.length !== 2 || parts[0] !== 'Bearer') {
ctx.status = 401;
return;
}
return async (ctx, next) => {
const authorization = ctx.headers.authorization;
const sessionCookie = ctx.cookies.get(options.session.cookieName);

const token = parts[1]!;
const origin = ctx.get('Origin');
const userId = await auth.validate(token, origin);
if (authorization) {
const parts = authorization.split(' ');

if (!userId) {
ctx.status = 401;
return;
}
if (parts.length !== 2 || parts[0] !== 'Bearer') {
ctx.status = 401;
return;
}

const token = parts[1]!;
const origin = ctx.get('Origin');
const userId = await auth.validate(token, origin);

ctx.state.userId = userId;
}
if (!userId) {
ctx.status = 401;
return;
}

return next();
ctx.state.user = { id: userId, authMode: 'token' };
} else if (sessionCookie) {
try {
const result = await jwtVerify<SessionCookiePayload>(sessionCookie, sessionKey);

if (result.payload.id && result.payload.app_access) {
ctx.state.user = { id: result.payload.id, authMode: 'cookie' };
}
} catch {}
}

return next();
};
};

export type AuthenticateState = { userId?: string };
export type AuthenticateOptions = { session: { cookieName: string, cookieSecret: string } };
export type AuthenticateState = { user?: { id: string, authMode: 'cookie' | 'token' } };

8 changes: 4 additions & 4 deletions src/lib/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export const rateLimit = async (ctx: ExtendedContext, numberOfProbes: number) =>
let rateLimiter: RateLimiterRedis;
let id: string;

if (ctx.state.userId) {
if (ctx.state.user?.id) {
rateLimiter = authenticatedRateLimiter;
id = ctx.state.userId;
id = ctx.state.user.id;
} else {
rateLimiter = anonymousRateLimiter;
id = requestIp.getClientIp(ctx.req) ?? '';
Expand All @@ -45,8 +45,8 @@ export const rateLimit = async (ctx: ExtendedContext, numberOfProbes: number) =>
setRateLimitHeaders(ctx, result, rateLimiter, numberOfProbes);
} catch (error) {
if (error instanceof RateLimiterRes) {
if (ctx.state.userId) {
const { isConsumed, requiredCredits, remainingCredits } = await consumeCredits(ctx.state.userId, error, numberOfProbes);
if (ctx.state.user?.id) {
const { isConsumed, requiredCredits, remainingCredits } = await consumeCredits(ctx.state.user.id, error, numberOfProbes);

if (isConsumed) {
const result = await rateLimiter.reward(id, requiredCredits);
Expand Down
5 changes: 3 additions & 2 deletions src/measurement/route/create-measurement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { getMeasurementRunner } from '../runner.js';
import { bodyParser } from '../../lib/http/middleware/body-parser.js';
import { corsAuthHandler } from '../../lib/http/middleware/cors.js';
import { validate } from '../../lib/http/middleware/validate.js';
import { authenticate } from '../../lib/http/middleware/authenticate.js';
import { authenticate, AuthenticateOptions } from '../../lib/http/middleware/authenticate.js';
import { schema } from '../schema/global-schema.js';
import type { ExtendedContext } from '../../types.js';

const sessionConfig = config.get<AuthenticateOptions['session']>('server.session');
const hostConfig = config.get<string>('server.host');
const runner = getMeasurementRunner();

Expand All @@ -26,5 +27,5 @@ const handle = async (ctx: ExtendedContext): Promise<void> => {
export const registerCreateMeasurementRoute = (router: Router): void => {
router
.options('/measurements', '/measurements', corsAuthHandler())
.post('/measurements', '/measurements', authenticate, bodyParser(), validate(schema), handle);
.post('/measurements', '/measurements', authenticate({ session: sessionConfig }), bodyParser(), validate(schema), handle);
};
Loading

0 comments on commit 2f33c29

Please sign in to comment.