diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..e605228 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,3 @@ +tasks: + - init: pnpm install + command: pnpm run start diff --git a/README.md b/README.md index 485fe00..aa43cf8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ # oauth-mock-server -A mocked oauth server for development and e2e testing +> :rotating_light: IMPORTANT: This tool should ONLY be used for development and testing setups! + +A mocked oauth server for development and e2e testing. + +## Config + +You can adjust the default configuration by placing a file `oauth-mock-server.json` in your current working directory: + +```json +{ + "realm": "my-project", + "users": [ + { + "id": "1", + "username": "toni", + "email": "toni@test.com", + "name": "Toni Tester" + }, + { + "id": "2", + "username": "alice", + "email": "alice@wonderland.org", + "name": "Alice Wonderland" + }, + { + "id": "3", + "username": "herbert", + "email": "her@bert.de", + "name": "Herbert" + } + ] +} +``` diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..d82b910 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,51 @@ +import fs from 'fs'; +import { cwd } from 'node:process'; +import path from 'path'; + +export type ConfigUser = { + id: string; + username: string; + email: string; + name: string; +}; + +export type Config = { + realm: string; + users: ConfigUser[]; +}; + +export function getConfig(): Config { + const defaultConfig = { + realm: 'my-project', + users: [ + { + id: '1', + username: 'toni', + email: 'toni@test.com', + name: 'Toni Tester', + }, + { + id: '2', + username: 'alice', + email: 'alice@wonderland.org', + name: 'Alice Wonderland', + }, + { + id: '3', + username: 'herbert', + email: 'her@bert.de', + name: 'Herbert', + }, + ], + }; + + const configPath = path.join(cwd(), 'oauth-mock-server.json'); + if (!fs.existsSync(configPath)) { + return defaultConfig; + } + + const rawConfig = fs.readFileSync(configPath); + const config = {}; + Object.assign(config, defaultConfig, JSON.parse(rawConfig.toString())); + return config; +} diff --git a/src/index.ts b/src/index.ts index 27d99c1..0a47e0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,41 +1,20 @@ import fastifyFormBody from '@fastify/formbody'; +import { getConfig } from 'config'; import fastify from 'fastify'; import jwt from 'jsonwebtoken'; +const config = getConfig(); + const server = fastify(); void server.register(fastifyFormBody); -const realm = 'bookyp'; // TODO: add way to load realm name from config - -// TODO: add way to load users from config -const users = [ - { - id: '1', - username: 'toni', - email: 'toni@test.com', - name: 'Toni Tester', - }, - { - id: '2', - username: 'alice', - email: 'alice@wonderland.org', - name: 'Alice Wonderland', - }, - { - id: '3', - username: 'herbert', - email: 'her@bert.de', - name: 'Herbert', - }, -]; - const jwtSecret = 'mySuperDuperSecret'; const randomString = () => (Math.random() + 1).toString(36).substring(7); let sessions: { code: string; user_id: string; access_token?: string }[] = []; -server.get(`/auth/realms/${realm}/protocol/openid-connect/auth`, async (request, reply) => { +server.get(`/auth/realms/${config.realm}/protocol/openid-connect/auth`, async (request, reply) => { const query = request.query as { redirect_uri: string; error?: string }; const template = ` @@ -84,7 +63,7 @@ server.get(`/auth/realms/${realm}/protocol/openid-connect/auth`, async (request,

Please login with one of the following usernames:

    - ${users + ${config.users .map( (user) => `
  • @@ -103,12 +82,13 @@ server.get(`/auth/realms/${realm}/protocol/openid-connect/auth`, async (request, server.all('/do-login', async (request, reply) => { const query = (request.body || request.query) as { username: string; redirect_uri: string }; - const user = users.find((u) => u.username === query.username) || users.find((u) => u.email === query.username); + const user = + config.users.find((u) => u.username === query.username) || config.users.find((u) => u.email === query.username); const redirect_uri = query.redirect_uri; if (!user) { await reply.redirect( - `/auth/realms/${realm}/protocol/openid-connect/auth?error=invalid_credentials&redirect_uri=${redirect_uri}`, + `/auth/realms/${config.realm}/protocol/openid-connect/auth?error=invalid_credentials&redirect_uri=${redirect_uri}`, ); return; } @@ -119,7 +99,7 @@ server.all('/do-login', async (request, reply) => { await reply.redirect(`${redirect_uri}?session_state=${sessionState}&code=${code}`); }); -server.post(`/auth/realms/${realm}/protocol/openid-connect/token`, async (request, reply) => { +server.post(`/auth/realms/${config.realm}/protocol/openid-connect/token`, async (request, reply) => { const body = request.body as { grant_type: string; code: string; @@ -139,7 +119,7 @@ server.post(`/auth/realms/${realm}/protocol/openid-connect/token`, async (reques const payload = { sub: session.user_id, // TODO check if valid (seems to work somehow) typ: 'Bearer', - aud: realm, + aud: config.realm, }; const accessToken = jwt.sign(payload, jwtSecret, { expiresIn: '1h' }); @@ -152,15 +132,15 @@ server.post(`/auth/realms/${realm}/protocol/openid-connect/token`, async (reques }; }); -server.get(`/auth/realms/${realm}/protocol/openid-connect/userinfo`, (request) => { +server.get(`/auth/realms/${config.realm}/protocol/openid-connect/userinfo`, (request) => { const headers = request.headers as { authorization: string }; const access_token = headers.authorization.replace('Bearer ', ''); const payload = jwt.verify(access_token, jwtSecret) as { sub: string }; const userId = payload.sub; - return users.find((s) => s.id === userId); + return config.users.find((s) => s.id === userId); }); -server.get(`/auth/realms/${realm}/protocol/openid-connect/logout`, async (request, reply) => { +server.get(`/auth/realms/${config.realm}/protocol/openid-connect/logout`, async (request, reply) => { const query = request.query as { redirect_uri: string }; await reply.redirect(query.redirect_uri); });