diff --git a/.env.example b/.env.example index 75dcf7683..bdacbe671 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,8 @@ #GITHUB_CLIENT_ID= #GITHUB_CLIENT_SECRET= +#AUTH_OIDC_PROVIDER='{"issuer":"http://localhost:8080/realms/dev_realm","clientId":"dev_client","clientSecret":"your_generated_secret"}' + #DATABASE_URL=postgresql://postgres:postgres-mqf3nzx@localhost:5438/postgres #REDIS_URL=redis://default:redis-mqf3nzx@localhost:6380 #KAFKA_BOOTSTRAP_SERVERS=localhost:19092 diff --git a/devenv/docker-compose.yml b/devenv/docker-compose.yml index d34f77d79..1cae5c710 100644 --- a/devenv/docker-compose.yml +++ b/devenv/docker-compose.yml @@ -30,7 +30,7 @@ services: max-size: 10m max-file: "3" healthcheck: - test: ["CMD-SHELL", "pg_isready", "-d", "postgres"] + test: ["CMD-SHELL", "pg_isready", "-d", "postgres", "-U", "postgres"] interval: 1s timeout: 10s retries: 10 @@ -38,44 +38,28 @@ services: - "${PG_PORT:-5438}:5432" volumes: - ./data/postgres:/var/lib/postgresql/data - jitsu-dev-zookeeper: - tty: true - image: wurstmeister/zookeeper:latest - expose: - - 2181 jitsu-dev-kafka: - tty: true - image: wurstmeister/kafka:latest - depends_on: - - jitsu-dev-zookeeper + image: bitnami/kafka:3.4 # ports: # - "19092:19092" # - "19093:19093" environment: - TERM: "xterm-256color" - KAFKA_ZOOKEEPER_CONNECT: jitsu-dev-zookeeper:2181 - - KAFKA_LISTENERS: INTERNAL://0.0.0.0:19093,OUTSIDE://0.0.0.0:19092 - KAFKA_ADVERTISED_LISTENERS: INTERNAL://jitsu-dev-kafka:19093,OUTSIDE://localhost:19092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,OUTSIDE:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_CFG_NODE_ID: 0 + KAFKA_CFG_PROCESS_ROLES: controller,broker + KAFKA_CFG_LISTENERS: PLAINTEXT://:19093,CONTROLLER://:9093,EXTERNAL://:19092 + KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://jitsu-dev-kafka:19093,EXTERNAL://localhost:19092 + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@jitsu-dev-kafka:9093 + KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true + ALLOW_PLAINTEXT_LISTENER: yes jitsu-dev-kafka-console: - tty: true - image: docker.redpanda.com/vectorized/console:master-173596f - links: - - "jitsu-dev-kafka:localhost" - restart: on-failure - entrypoint: /bin/sh - command: -c "echo \"$$CONSOLE_CONFIG_FILE\" > /tmp/config.yml; /app/console" - environment: - TERM: "xterm-256color" - CONFIG_FILEPATH: /tmp/config.yml - CONSOLE_CONFIG_FILE: | - kafka: - brokers: ["jitsu-dev-kafka:19093"] + image: docker.redpanda.com/redpandadata/console:latest ports: - "${KAFKA_CONSOLE_PORT:-3032}:8080" + environment: + KAFKA_BROKERS: "jitsu-dev-kafka:19093" depends_on: - jitsu-dev-kafka @@ -105,3 +89,11 @@ services: depends_on: - jitsu-dev-postgres - jitsu-dev-kafka + keycloak: + command: start-dev + image: "quay.io/keycloak/keycloak:26.0.4" + environment: + - KC_BOOTSTRAP_ADMIN_PASSWORD=admin + - KC_BOOTSTRAP_ADMIN_USERNAME=admin + ports: + - "8080:8080" diff --git a/webapps/console/lib/nextauth.config.ts b/webapps/console/lib/nextauth.config.ts index 1b45c6f23..a6c6c0c17 100644 --- a/webapps/console/lib/nextauth.config.ts +++ b/webapps/console/lib/nextauth.config.ts @@ -2,6 +2,7 @@ import GithubProvider from "next-auth/providers/github"; import CredentialsProvider from "next-auth/providers/credentials"; import { NextAuthOptions, User } from "next-auth"; import { db } from "./server/db"; +import { OIDCProvider, ParseJSONConfigFromEnv } from "./oidc"; import { checkHash, createHash, hash, requireDefined } from "juava"; import { ApiError } from "./shared/errors"; import { getServerLog } from "./server/log"; @@ -15,6 +16,7 @@ const crypto = require("crypto"); const log = getServerLog("auth"); export const githubLoginEnabled = !!process.env.GITHUB_CLIENT_ID; +export const oidcLoginConfig = ParseJSONConfigFromEnv(process.env.AUTH_OIDC_PROVIDER as string); export const credentialsLoginEnabled = isTruish(process.env.ENABLE_CREDENTIALS_LOGIN) || !!(process.env.SEED_USER_EMAIL && process.env.SEED_USER_PASSWORD); @@ -25,6 +27,8 @@ const githubProvider = githubLoginEnabled }) : undefined; +const oidcProvider = oidcLoginConfig ? OIDCProvider(oidcLoginConfig) : undefined; + function toId(email: string) { return hash("sha256", email.toLowerCase().trim()); } @@ -143,7 +147,7 @@ function generateSecret(base: (string | undefined)[]) { export const nextAuthConfig: NextAuthOptions = { // Configure one or more authentication providers - providers: [githubProvider, credentialsProvider].filter(provider => !!provider) as any, + providers: [githubProvider, oidcProvider, credentialsProvider].filter(provider => !!provider) as any, pages: { error: "/error/auth", // Error code passed in query string as ?error= signIn: "/signin", // Displays signin buttons diff --git a/webapps/console/lib/oidc.ts b/webapps/console/lib/oidc.ts new file mode 100644 index 000000000..127933e79 --- /dev/null +++ b/webapps/console/lib/oidc.ts @@ -0,0 +1,68 @@ +import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth"; +import { ApiError } from "./shared/errors"; + +export interface OIDCProfile extends Record { + sub: string; + name: string; + preferred_username: string; + nickname: string; + email: string; + picture: string; +} + +export type OIDCConfig

= OAuthUserConfig

& Required, "issuer">>; + +/** + * Creates an OAuth configuration for an OpenID Connect (OIDC) Discovery compliant provider. + * + * @template P - The type of the profile, extending `OIDCProfile`. + * + * @param {OIDCConfig

} options - The user configuration options for OAuth authentication. + * + * @returns {OAuthConfig

} - An OIDC provider NextAuthJS valid configuration. + * + * @throws {ApiError} - Throws an error if the required fields `issuer`, `clientId`, or `clientSecret` + * are not provided in the options parameter. + * + * @description + * Initializes an OAuth configuration object for a generic OIDC provider that is compliant with the OIDC Discovery. It requires + * the `issuer` (the issuer domain in valid URL format), `clientId`, and `clientSecret` fields in the options. This configuration + * includes default settings for handling the PKCE and state checks and provides + * a profile extraction mechanism. + * + * The well-known configuration endpoint for the provider is automatically set based on the issuer, and + * the default authorization request includes scopes for OpenID, email, and profile information. + */ +export function OIDCProvider

(options: OIDCConfig

): OAuthConfig

{ + if (!options.issuer || !options.clientId || !options.clientSecret) { + throw new ApiError("Malformed OIDC config: issuer, clientId, and clientSecret are required"); + } + + return { + id: "oidc", + name: "OIDC", + wellKnown: `${options.issuer}/.well-known/openid-configuration`, + type: "oauth", + authorization: { params: { scope: "openid email profile" } }, + checks: ["pkce", "state"], + idToken: true, + profile(profile) { + return { + id: profile.sub, + name: profile.name ?? profile.preferred_username ?? profile.nickname, + email: profile.email, + image: profile.picture, + }; + }, + options, + }; +} + +export function ParseJSONConfigFromEnv

(env: string): OIDCConfig

| undefined { + try { + return env && env != '""' ? (JSON.parse(env) as OIDCConfig

) : undefined; + } catch (error: unknown) { + console.error("Failed to parse JSON config from env", error); + return undefined; + } +} diff --git a/webapps/console/pages/signin.tsx b/webapps/console/pages/signin.tsx index 92b6773e2..ac9530d0c 100644 --- a/webapps/console/pages/signin.tsx +++ b/webapps/console/pages/signin.tsx @@ -4,12 +4,12 @@ import { Button, Input } from "antd"; import { useAppConfig } from "../lib/context"; import { AlertTriangle } from "lucide-react"; import Link from "next/link"; -import { GithubOutlined } from "@ant-design/icons"; +import { GithubOutlined, KeyOutlined } from "@ant-design/icons"; import React, { useState } from "react"; import { feedbackError } from "../lib/ui"; import { useRouter } from "next/router"; import { branding } from "../lib/branding"; -import { credentialsLoginEnabled, githubLoginEnabled } from "../lib/nextauth.config"; +import { credentialsLoginEnabled, githubLoginEnabled, oidcLoginConfig } from "../lib/nextauth.config"; import { useQuery } from "@tanstack/react-query"; function JitsuLogo() { @@ -92,7 +92,34 @@ function GitHubSignIn() { ); } -const NextAuthSignInPage = ({ csrfToken, providers: { github, credentials } }) => { +function OIDCSignIn() { + const [loading, setLoading] = useState(false); + const router = useRouter(); + return ( +

+ +
+ ); +} + +const NextAuthSignInPage = ({ csrfToken, providers: { github, oidc, credentials } }) => { const router = useRouter(); const nextAuthSession = useSession(); const app = useAppConfig(); @@ -117,17 +144,18 @@ const NextAuthSignInPage = ({ csrfToken, providers: { github, credentials } }) =
-
+
{credentials.enabled && } - {credentials.enabled && github.enabled &&
} + {credentials.enabled && (github.enabled || oidc.enabled) &&
} {github.enabled && } + {oidc.enabled && }
{router.query.error && (
Something went wrong. Please try again. Error code: {router.query.error}
)} - {!app.disableSignup && github.enabled && ( + {!app.disableSignup && (github.enabled || oidc.enabled) && (
Automatic signup is enabled for this instance. Sign in with github and if you don't have an account, a new account will be created automatically. This account won't have any access to pre-existing project unless the @@ -142,7 +170,7 @@ export async function getServerSideProps(context) { if (process.env.FIREBASE_AUTH) { throw new Error(`Firebase auth is enabled. This page should not be used.`); } - if (!githubLoginEnabled && !credentialsLoginEnabled) { + if (!githubLoginEnabled && !credentialsLoginEnabled && !oidcLoginConfig) { throw new Error(`No auth providers are enabled found. Available providers: github, credentials`); } return { @@ -155,6 +183,9 @@ export async function getServerSideProps(context) { github: { enabled: githubLoginEnabled, }, + oidc: { + enabled: !!oidcLoginConfig, + }, }, publicPage: true, },