Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add terraform configuration for Keycloak for the local development environment #1085

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
# Prisma doesn't currently support giving a default value in case DATABASE_URL is empty, see https://github.com/prisma/prisma/issues/222
DATABASE_URL=

# API URL
NEXT_PUBLIC_API_URL=http://localhost:5000/api/

##########################################################################
# Compulsory in production (aka when NODE_ENV=production):
##########################################################################

# Keycloak variables for the API and UI
KEYCLOAK_URL=
KEYCLOAK_REALM=
Expand All @@ -18,20 +25,6 @@ KEYCLOAK_CLIENT_SECRET_API=
KEYCLOAK_CLIENT_ID_UI=
KEYCLOAK_CLIENT_SECRET_UI=

# API URL
NEXT_PUBLIC_API_URL=http://localhost:5000/api/

##########################################################################
# Compulsory when running Playwright tests:
##########################################################################
# E2E test user credentials
E2E_USER_USERNAME=
E2E_USER_PASSWORD=

##########################################################################
# Compulsory in production (aka when NODE_ENV=production):
##########################################################################

# NEXTAUTH specific settings
NEXTAUTH_URL= # http://localhost:3000
NEXTAUTH_SECRET=
Expand Down
8 changes: 0 additions & 8 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@ jobs:
environment: test-environment
env:
DATABASE_URL: ${{ vars.DATABASE_URL }}
E2E_USER_USERNAME: ${{ secrets.E2E_USER_USERNAME }}
E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
KEYCLOAK_URL: ${{ vars.KEYCLOAK_URL }}
KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }}
KEYCLOAK_CLIENT_ID_UI: ${{ secrets.KEYCLOAK_CLIENT_ID_UI }}
KEYCLOAK_CLIENT_SECRET_UI: ${{ secrets.KEYCLOAK_CLIENT_SECRET_UI }}
KEYCLOAK_CLIENT_ID_API: ${{ secrets.KEYCLOAK_CLIENT_ID_API }}
KEYCLOAK_CLIENT_SECRET_API: ${{ secrets.KEYCLOAK_CLIENT_SECRET_API }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ jobs:
env:
NODE_ENV: test
NEXT_PUBLIC_API_URL: http://localhost:5000
KEYCLOAK_URL: https://keycloak.example.com
KEYCLOAK_REALM: example
KEYCLOAK_CLIENT_ID_UI: example-ui
KEYCLOAK_CLIENT_SECRET_UI: example-ui-secret

- name: Run tests
run: npm run test
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,7 @@ yarn.lock

# Playwright mounted files
playwright-artifacts/

# Terraform
terraform/.terraform/*
terraform/terraform.tfstate*
4 changes: 4 additions & 0 deletions .reuse/dep5
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,7 @@ License: CC-BY-4.0
Files: requirements.txt
Copyright: 2025 Double Open Oy
License: CC-BY-4.0

Files: terraform/.terraform.lock.hcl
Copyright: 2025 Double Open Oy
License: CC-BY-4.0
8 changes: 0 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,6 @@ To run this project you will need Node.js, npm and Docker installed.

```shell
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
KEYCLOAK_URL=
KEYCLOAK_REALM=
KEYCLOAK_CLIENT_ID_API=
KEYCLOAK_CLIENT_SECRET_API=
KEYCLOAK_CLIENT_ID_UI=
KEYCLOAK_CLIENT_SECRET_UI=
E2E_USER_USERNAME=
E2E_USER_PASSWORD=
```

See [.env.example](https://github.com/doubleopen-project/dos/blob/main/.env.example) file for other non-compulsory configurable variables.
Expand Down
4 changes: 3 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "MIT",
"scripts": {
"build": "tsup --format cjs",
"dev": "tsup src/server.ts --format cjs --watch --onSuccess \"node --env-file ../../.env dist/server.js\"",
"dev": "dotenv -e ../../.env -- tsup src/server.ts --format cjs --watch --onSuccess \"node dist/server.js\"",
"lint": "tsc --noEmit && eslint \"src/**/*.ts*\"",
"start": "node --env-file-if-exists ../../.env dist/src/server.js",
"test": "mocha dist/tests --exit --timeout 10000",
Expand All @@ -16,6 +16,7 @@
"adm-zip": "0.5.16",
"braces": "^3.0.3",
"bull": "4.16.5",
"common-helpers": "*",
"compression": "1.8.0",
"connect-pg-simple": "10.0.0",
"cookie-parser": "1.4.7",
Expand Down Expand Up @@ -62,6 +63,7 @@
"@types/passport-local": "1.0.38",
"@types/swagger-ui-express": "4.1.8",
"chai": "5.2.0",
"dotenv-cli": "8.0.0",
"eslint-config-custom-server": "*",
"mocha": "11.1.0",
"tsconfig": "*",
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/@types/express.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ declare namespace Express {
roles: string[];
};
resource_access: {
[process.env.KEYCLOAK_CLIENT_ID_API]: {
[process.env.KEYCLOAK_CLIENT_ID_API ||
"dos-dev-api"]: {
roles: string[];
};
account: {
Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/config/keycloak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: MIT

import { authConfig } from "common-helpers";
import genFunc from "connect-pg-simple";
import type { Request, Response } from "express";
import session from "express-session";
Expand All @@ -20,9 +21,9 @@ const memoryStore = new PostgresqlStore({
});

const keycloakConfig: KeycloakConfig = {
realm: process.env.KEYCLOAK_REALM!,
resource: process.env.KEYCLOAK_CLIENT_ID_API!,
"auth-server-url": process.env.KEYCLOAK_URL!,
realm: authConfig.realm,
resource: authConfig.clientIdAPI,
"auth-server-url": authConfig.url,
"bearer-only": true,
"confidential-port": 0,
"ssl-required": "external",
Expand Down
26 changes: 12 additions & 14 deletions apps/api/src/helpers/keycloak_queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@

import { Zodios, ZodiosResponseByAlias } from "@zodios/core";
import { isAxiosError } from "axios";
import { authConfig } from "common-helpers";
import NodeCache from "node-cache";
import { keycloakAPI, type ClientCredentialsToken } from "validation-helpers";
import { CustomError } from "./custom_error";

const kcClient = new Zodios(
process.env.KEYCLOAK_URL || "https://auth.dev.doubleopen.io/",
keycloakAPI,
);
const kcClient = new Zodios(authConfig.url, keycloakAPI);

const cache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 60 });

Expand All @@ -34,16 +32,16 @@ export const getAccessToken = async (): Promise<ClientCredentialsToken> => {
try {
const accessToken = (await kcClient.PostToken(
{
client_id: process.env.KEYCLOAK_CLIENT_ID_API!,
client_id: authConfig.clientIdAPI,
grant_type: "client_credentials",
client_secret: process.env.KEYCLOAK_CLIENT_SECRET_API!,
client_secret: authConfig.clientSecretAPI,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
params: {
realm: process.env.KEYCLOAK_REALM!,
realm: authConfig.realm,
},
},
)) as ClientCredentialsToken; // The endpoint provides a union type, but we know it's a ClientCredentialsToken with this type of request
Expand Down Expand Up @@ -100,7 +98,7 @@ export const logoutUser = async (
const token = await getAccessToken();
await kcClient.LogoutUser(undefined, {
params: {
realm: process.env.KEYCLOAK_REALM!,
realm: authConfig.realm,
id: userId,
},
headers: {
Expand Down Expand Up @@ -173,7 +171,7 @@ export const createUser = async (data: {
const token = await getAccessToken();
await kcClient.CreateUser(data, {
params: {
realm: process.env.KEYCLOAK_REALM!,
realm: authConfig.realm,
},
headers: {
Authorization: "Bearer " + token.access_token,
Expand Down Expand Up @@ -239,7 +237,7 @@ export const deleteUser = async (userId: string): Promise<boolean> => {
const token = await getAccessToken();
await kcClient.DeleteUser(undefined, {
params: {
realm: process.env.KEYCLOAK_REALM!,
realm: authConfig.realm,
id: userId,
},
headers: {
Expand Down Expand Up @@ -294,7 +292,7 @@ export const getRealmRoles = async (): Promise<RealmRole[]> => {
const token = await getAccessToken();
roles = await kcClient.GetRealmRoles({
params: {
realm: process.env.KEYCLOAK_REALM!,
realm: authConfig.realm,
},
headers: {
Authorization: "Bearer " + token.access_token,
Expand Down Expand Up @@ -360,7 +358,7 @@ export const addRealmRolesToUser = async (

await kcClient.AddRealmRoleToUser(roles, {
params: {
realm: process.env.KEYCLOAK_REALM!,
realm: authConfig.realm,
id: userId,
},
headers: {
Expand Down Expand Up @@ -420,7 +418,7 @@ export const getUsers = async (
const token = await getAccessToken();
users = await kcClient.GetUsers({
params: {
realm: process.env.KEYCLOAK_REALM!,
realm: authConfig.realm,
},
queries: {
username: username,
Expand Down Expand Up @@ -496,7 +494,7 @@ export const updateUser = async (
const token = await getAccessToken();
await kcClient.UpdateUser(data, {
params: {
realm: process.env.KEYCLOAK_REALM!,
realm: authConfig.realm,
id: userId,
},
headers: {
Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/middlewares/authz_permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: MIT

import { authConfig } from "common-helpers";
import { NextFunction, Request, Response } from "express";
import { authorizationByPermission } from "keycloak-authorization-services";
import log from "loglevel";
Expand All @@ -16,13 +17,13 @@ export const authzPermission = (permission: {
scopes: string[];
}) => {
const config = {
baseUrl: process.env.KEYCLOAK_URL!,
realm: process.env.KEYCLOAK_REALM!,
baseUrl: authConfig.url,
realm: authConfig.realm,
};

const options = {
permission,
audience: process.env.KEYCLOAK_CLIENT_ID_API!,
audience: authConfig.clientIdAPI,
};

return async function customAuthorizationMiddleware(
Expand Down
12 changes: 5 additions & 7 deletions apps/api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,14 @@ import { onStartUp } from "./helpers/on_start_up";
import { adminRouter, authRouter, scannerRouter, userRouter } from "./routes";
import QueueService from "./services/queue";

const requiredEnvVars: string[] = [
"DATABASE_URL",
"KEYCLOAK_URL",
"KEYCLOAK_REALM",
"KEYCLOAK_CLIENT_ID_API",
"KEYCLOAK_CLIENT_SECRET_API",
];
const requiredEnvVars: string[] = ["DATABASE_URL"];

if (process.env.NODE_ENV === "production") {
requiredEnvVars.push(
"KEYCLOAK_URL",
"KEYCLOAK_REALM",
"KEYCLOAK_CLIENT_ID_API",
"KEYCLOAK_CLIENT_SECRET_API",
"SESSION_SECRET",
"COOKIE_SECRET",
"SPACES_ENDPOINT",
Expand Down
13 changes: 7 additions & 6 deletions apps/api/tests/e2e/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ import {
import test, { expect } from "@playwright/test";
import { Zodios, ZodiosInstance } from "@zodios/core";
import AdmZip from "adm-zip";
import { authConfig } from "common-helpers";
import { dosAPI, userAPI } from "validation-helpers";

/**
* Construct Zodios callers for the API endpoints to easily call them in the tests.
*/

const server = process.env.KEYCLOAK_URL;
const realm = process.env.KEYCLOAK_REALM;
const clientId = process.env.KEYCLOAK_CLIENT_ID_API;
const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET_API;
const username = process.env.E2E_USER_USERNAME;
const password = process.env.E2E_USER_PASSWORD;
const server = authConfig.url;
const realm = authConfig.realm;
const clientId = authConfig.clientIdUI;
const clientSecret = authConfig.clientSecretUI;
const username = "test-user";
const password = "test-user";
const baseUrl = process.env.CI ? "http://api:3001" : "http://localhost:5000";

if (!server || !realm || !clientId || !clientSecret || !username || !password) {
Expand Down
1 change: 1 addition & 0 deletions apps/clearance_ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"common-helpers": "*",
"generate-password": "1.7.1",
"js-yaml": "4.1.0",
"lodash.debounce": "4.0.8",
Expand Down
34 changes: 5 additions & 29 deletions apps/clearance_ui/src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,16 @@

import { Zodios } from "@zodios/core";
import { isAxiosError } from "axios";
import { authConfig } from "common-helpers";
import NextAuth from "next-auth";
import type { JWT } from "next-auth/jwt";
import KeycloakProvider from "next-auth/providers/keycloak";
import { keycloakAPI, type Token } from "validation-helpers";

const keycloakUrl = process.env.KEYCLOAK_URL;

if (!keycloakUrl) {
throw new Error("KEYCLOAK_URL not set");
}

const keycloakRealm = process.env.KEYCLOAK_REALM;

if (!keycloakRealm) {
throw new Error("KEYCLOAK_REALM not set");
}

const keycloakClientIdUi = process.env.KEYCLOAK_CLIENT_ID_UI;

if (!keycloakClientIdUi) {
throw new Error("KEYCLOAK_CLIENT_ID_UI not set");
}

const keycloakClientSecretUi = process.env.KEYCLOAK_CLIENT_SECRET_UI;

if (!keycloakClientSecretUi) {
throw new Error("KEYCLOAK_CLIENT_SECRET_UI not set");
}
const keycloakUrl = authConfig.url;
const keycloakRealm = authConfig.realm;
const keycloakClientIdUi = authConfig.clientIdUI;
const keycloakClientSecretUi = authConfig.clientSecretUI;

const kcClient = new Zodios(keycloakUrl, keycloakAPI);

Expand All @@ -49,12 +31,6 @@ const kcClient = new Zodios(keycloakUrl, keycloakAPI);
*/
async function refreshAccessToken(token: JWT) {
let retries = 3;
if (!keycloakClientIdUi) {
throw new Error("KEYCLOAK_CLIENT_ID_UI not set");
}
if (!keycloakRealm) {
throw new Error("KEYCLOAK_REALM not set");
}

while (retries > 0) {
try {
Expand Down
Loading