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

feat(@jitsu/console): add generic OIDC provider SSO #1152

Merged
merged 12 commits into from
Dec 18, 2024
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
Loading