Skip to content

Commit

Permalink
feat(@jitsu/console): add generic OIDC provider SSO (#1152)
Browse files Browse the repository at this point in the history
* feat: add generic oidc sso option
* chore: add AUTH_OIDC_PROVIDER var at .env example
* chore: ensure pg healthcheck has same user on devenv compose
* chore: remove zookeeper dependency on kafka at devenv compose
* chore: add keycloak service at devenv compose
* chore: ensure correct parse of AUTH_OIDC_PROVIDER env
* fix: change oidc oauth types import path
* Fix for auth provider check
* visual tweaks
  • Loading branch information
pedroyremolo authored Dec 18, 2024
1 parent 26f79e6 commit 34420de
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 38 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#GITHUB_CLIENT_ID=<Make your own client>
#GITHUB_CLIENT_SECRET=<Make your own client>

#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
Expand Down
52 changes: 22 additions & 30 deletions devenv/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,52 +30,36 @@ 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
ports:
- "${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

Expand Down Expand Up @@ -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"
6 changes: 5 additions & 1 deletion webapps/console/lib/nextauth.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);

Expand All @@ -25,6 +27,8 @@ const githubProvider = githubLoginEnabled
})
: undefined;

const oidcProvider = oidcLoginConfig ? OIDCProvider(oidcLoginConfig) : undefined;

function toId(email: string) {
return hash("sha256", email.toLowerCase().trim());
}
Expand Down Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions webapps/console/lib/oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
import { ApiError } from "./shared/errors";

export interface OIDCProfile extends Record<string, any> {
sub: string;
name: string;
preferred_username: string;
nickname: string;
email: string;
picture: string;
}

export type OIDCConfig<P> = OAuthUserConfig<P> & Required<Pick<OAuthConfig<P>, "issuer">>;

/**
* Creates an OAuth configuration for an OpenID Connect (OIDC) Discovery compliant provider.
*
* @template P - The type of the profile, extending `OIDCProfile`.
*
* @param {OIDCConfig<P>} options - The user configuration options for OAuth authentication.
*
* @returns {OAuthConfig<P>} - 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<P extends OIDCProfile>(options: OIDCConfig<P>): OAuthConfig<P> {
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<P extends OIDCProfile>(env: string): OIDCConfig<P> | undefined {
try {
return env && env != '""' ? (JSON.parse(env) as OIDCConfig<P>) : undefined;
} catch (error: unknown) {
console.error("Failed to parse JSON config from env", error);
return undefined;
}
}
45 changes: 38 additions & 7 deletions webapps/console/pages/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -92,7 +92,34 @@ function GitHubSignIn() {
);
}

const NextAuthSignInPage = ({ csrfToken, providers: { github, credentials } }) => {
function OIDCSignIn() {
const [loading, setLoading] = useState(false);
const router = useRouter();
return (
<div className="space-y-4">
<Button
className="w-full"
icon={<KeyOutlined />}
loading={loading}
onClick={async () => {
try {
setLoading(true);
await signIn("oidc");
await router.push("/");
} catch (e: any) {
feedbackError("Failed to sign in with SSO provider", e);
} finally {
setLoading(false);
}
}}
>
Sign in with SSO
</Button>
</div>
);
}

const NextAuthSignInPage = ({ csrfToken, providers: { github, oidc, credentials } }) => {
const router = useRouter();
const nextAuthSession = useSession();
const app = useAppConfig();
Expand All @@ -117,17 +144,18 @@ const NextAuthSignInPage = ({ csrfToken, providers: { github, credentials } }) =
<div className="space-y-2 flex justify-center h-16">
<JitsuLogo />
</div>
<div>
<div className={"flex flex-col gap-1.5"}>
{credentials.enabled && <CredentialsForm />}
{credentials.enabled && github.enabled && <hr className="my-8" />}
{credentials.enabled && (github.enabled || oidc.enabled) && <hr className="my-4" />}
{github.enabled && <GitHubSignIn />}
{oidc.enabled && <OIDCSignIn />}
</div>
{router.query.error && (
<div className="text-error">
Something went wrong. Please try again. Error code: <code>{router.query.error}</code>
</div>
)}
{!app.disableSignup && github.enabled && (
{!app.disableSignup && (github.enabled || oidc.enabled) && (
<div className="text-center text-textLight text-xs">
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
Expand All @@ -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 {
Expand All @@ -155,6 +183,9 @@ export async function getServerSideProps(context) {
github: {
enabled: githubLoginEnabled,
},
oidc: {
enabled: !!oidcLoginConfig,
},
},
publicPage: true,
},
Expand Down

0 comments on commit 34420de

Please sign in to comment.