Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
dbd6086
feat: SCRAM-SHA-256, TOTP 2FA + biometric (WebAuthn) login
ValwareIRC May 2, 2026
af2064b
feat: tic-tac-toe (compatible with KiwiIRC plugin)
ValwareIRC May 2, 2026
b6e6bd9
feat: surface tic-tac-toe button in the PM header action row
ValwareIRC May 2, 2026
25f5b0d
2FA: confirm-before-remove, block removing last credential while enabled
ValwareIRC May 3, 2026
d82124b
Request draft/account-2fa CAP so the client merges it into server.cap…
ValwareIRC May 4, 2026
cca2abd
Move 2FA management into UserSettings → Account; raise 2FA modal z-in…
ValwareIRC May 4, 2026
b096e0b
Don't send spurious AUTHENTICATE + after SCRAM server-final
ValwareIRC May 4, 2026
8fd71bb
feat(auth): SASL IRCV3BEARER + per-server OAuth2/OIDC sign-in
ValwareIRC May 9, 2026
68140b4
feat(auth): bake OAuth config into the build for lock-mode deployments
ValwareIRC May 9, 2026
fe7e9e5
refactor(EditServerModal): tabbed left-sidebar layout
ValwareIRC May 9, 2026
7b9034e
feat(auth): opaque-token (GitHub) + id_token (Gmail) OAuth paths
ValwareIRC May 9, 2026
6223a7c
feat(auth): static /oauth/callback page
ValwareIRC May 9, 2026
581dbfe
Merge main into feat/auth-modern
ValwareIRC May 9, 2026
b16a4d5
merge feat/auth-modern (deploy-only; not for push)
ValwareIRC May 9, 2026
a3863d4
feat(auth): smart 2FA step-up — IRCV3BEARER step-up + reject same-fac…
ValwareIRC May 10, 2026
3af9830
Merge remote-tracking branch 'origin/main' into feat/oauth2-sasl
ValwareIRC May 11, 2026
6779045
Fix cmd→command for 2FA NOTE projection
ValwareIRC May 11, 2026
895db6e
Merge remote-tracking branch 'origin/main' into feat/oauth2-sasl
ValwareIRC May 11, 2026
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
15 changes: 15 additions & 0 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ VITE_HIDE_SERVER_LIST=true
# Optional comma-separated list of trusted media URLs
# Useful for chat bridges like Matterbridge or Matrix bridges that host media
VITE_TRUSTED_MEDIA_URLS="https://matterbridge.example.com,https://matrix-media.example.com"

# Optional OAuth2 / OIDC defaults. Only surfaced when VITE_HIDE_SERVER_LIST=true,
# i.e. single-server lock-mode. Users see a "Sign in with <label>" button
# instead of having to enter the issuer/client_id themselves. Requires the
# IRC server to support SASL IRCV3BEARER (e.g. obbyircd's oauth-provider).
VITE_DEFAULT_OAUTH_PROVIDER_LABEL="Logto"
VITE_DEFAULT_OAUTH_ISSUER="https://my-tenant.logto.app/oidc"
VITE_DEFAULT_OAUTH_CLIENT_ID="m0obbyircd1234"
# Optional, defaults to "openid"
VITE_DEFAULT_OAUTH_SCOPES="openid"
# Optional, defaults to <origin>/oauth/callback. Must be registered with the IdP.
VITE_DEFAULT_OAUTH_REDIRECT_URI="https://chat.example.com/oauth/callback"
```

### Docker
Expand All @@ -47,6 +59,9 @@ docker build \
--build-arg VITE_DEFAULT_IRC_CHANNELS="#general,#random" \
--build-arg VITE_HIDE_SERVER_LIST=false \
--build-arg VITE_TRUSTED_MEDIA_URLS="https://matterbridge.example.com,https://matrix-media.example.com" \
--build-arg VITE_DEFAULT_OAUTH_PROVIDER_LABEL="Logto" \
--build-arg VITE_DEFAULT_OAUTH_ISSUER="https://my-tenant.logto.app/oidc" \
--build-arg VITE_DEFAULT_OAUTH_CLIENT_ID="m0obbyircd1234" \
-t obsidianirc .
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
```

Expand Down
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ ARG VITE_DEFAULT_IRC_SERVER_NAME
ARG VITE_DEFAULT_IRC_CHANNELS
ARG VITE_HIDE_SERVER_LIST
ARG VITE_TRUSTED_MEDIA_URLS
ARG VITE_DEFAULT_OAUTH_PROVIDER_LABEL
ARG VITE_DEFAULT_OAUTH_ISSUER
ARG VITE_DEFAULT_OAUTH_CLIENT_ID
ARG VITE_DEFAULT_OAUTH_SCOPES
ARG VITE_DEFAULT_OAUTH_REDIRECT_URI

ENV VITE_DEFAULT_IRC_SERVER=$VITE_DEFAULT_IRC_SERVER
ENV VITE_DEFAULT_IRC_SERVER_NAME=$VITE_DEFAULT_IRC_SERVER_NAME
ENV VITE_DEFAULT_IRC_CHANNELS=$VITE_DEFAULT_IRC_CHANNELS
ENV VITE_HIDE_SERVER_LIST=$VITE_HIDE_SERVER_LIST
ENV VITE_TRUSTED_MEDIA_URLS=$VITE_TRUSTED_MEDIA_URLS
ENV VITE_DEFAULT_OAUTH_PROVIDER_LABEL=$VITE_DEFAULT_OAUTH_PROVIDER_LABEL
ENV VITE_DEFAULT_OAUTH_ISSUER=$VITE_DEFAULT_OAUTH_ISSUER
ENV VITE_DEFAULT_OAUTH_CLIENT_ID=$VITE_DEFAULT_OAUTH_CLIENT_ID
ENV VITE_DEFAULT_OAUTH_SCOPES=$VITE_DEFAULT_OAUTH_SCOPES
ENV VITE_DEFAULT_OAUTH_REDIRECT_URI=$VITE_DEFAULT_OAUTH_REDIRECT_URI

WORKDIR /app
COPY package*.json ./
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from "react";
import { Route, Routes } from "react-router-dom";
import AppLayout from "./components/layout/AppLayout";
import { ServerNoticesPopup } from "./components/message/ServerNoticesPopup";
import OAuthCallback from "./components/OAuthCallback";
import PrivacyPolicy from "./components/PrivacyPolicy";
import AddServerModal from "./components/ui/AddServerModal";
import ChannelListModal from "./components/ui/ChannelListModal";
Expand Down Expand Up @@ -294,6 +295,7 @@ const App: React.FC = () => {
<div className="h-screen overflow-hidden">
<Routes>
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/oauth/callback" element={<OAuthCallback />} />
<Route
path="/*"
element={
Expand Down
60 changes: 60 additions & 0 deletions src/components/OAuthCallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type React from "react";
import { useEffect, useState } from "react";
import type { OAuthCallbackMessage } from "../lib/oauth";

// Loaded at /oauth/callback inside the popup window the OAuth flow opened.
// Pulls `code` and `state` (or `error`) from the query string, sends them
// back to the opener via postMessage, then closes itself.
export const OAuthCallback: React.FC = () => {
const [message, setMessage] = useState("Completing sign-in...");

useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get("code") ?? undefined;
const state = params.get("state") ?? "";
const error = params.get("error") ?? undefined;
const errorDescription = params.get("error_description") ?? undefined;

const payload: OAuthCallbackMessage = {
type: "obsidianirc:oauth-callback",
state,
code,
error,
errorDescription,
};

if (window.opener && window.opener !== window) {
try {
window.opener.postMessage(payload, window.location.origin);
setMessage("Sign-in complete. You can close this window.");
setTimeout(() => {
try {
window.close();
} catch {}
}, 300);
return;
} catch (err) {
setMessage(
`Could not return tokens to the main window: ${
err instanceof Error ? err.message : String(err)
}`,
);
return;
}
}
setMessage(
"OAuth callback was loaded outside its popup. You can close this tab.",
);
}, []);

return (
<div className="flex h-screen items-center justify-center bg-discord-dark-200 text-discord-text-normal">
<div className="max-w-md p-6 text-center">
<h1 className="text-xl font-bold mb-2">OAuth sign-in</h1>
<p className="text-discord-text-muted">{message}</p>
</div>
</div>
);
};

export default OAuthCallback;
22 changes: 22 additions & 0 deletions src/components/ui/AddServerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { useEffect, useState } from "react";
import { FaQuestionCircle } from "react-icons/fa";
import BaseModal from "../../lib/modal/BaseModal";
import { Button, ModalBody, ModalFooter } from "../../lib/modal/components";
import { getBuiltinOAuthConfig } from "../../lib/oauth";
import { isTauri } from "../../lib/platformUtils";
import useStore from "../../store";
import type { ServerOAuthConfig } from "../../types";
import { OAuthSection } from "./OAuthSection";
import { TextInput } from "./TextInput";

export const AddServerModal: React.FC = () => {
Expand Down Expand Up @@ -40,9 +43,19 @@ export const AddServerModal: React.FC = () => {
);
const [registerEmail, setRegisterEmail] = useState("");
const [registerPassword, setRegisterPassword] = useState("");
const [oauthConfig, setOauthConfig] = useState<ServerOAuthConfig | undefined>(
undefined,
);

const [error, setError] = useState("");

// In single-server lock-mode, surface the deployer-baked OAuth config so
// the welcome screen exposes a "Sign in with X" CTA instead of the full
// editable provider form.
const lockedOauth = __HIDE_SERVER_LIST__
? getBuiltinOAuthConfig()
: undefined;

useEffect(() => {
setServerName(prefillServerDetails?.name || "");
setServerHost(prefillServerDetails?.host || "");
Expand Down Expand Up @@ -121,6 +134,7 @@ export const AddServerModal: React.FC = () => {
registerEmail,
registerPassword,
true,
oauthConfig,
);
toggleAddServerModal(false);
} catch (err) {
Expand Down Expand Up @@ -238,6 +252,14 @@ export const AddServerModal: React.FC = () => {
</>
)}

{lockedOauth && (
<OAuthSection
initial={oauthConfig}
onChange={setOauthConfig}
locked={lockedOauth}
/>
)}

<div className="mb-4">
<label className="block text-discord-text-muted text-sm font-medium mb-1">
Nickname
Expand Down
Loading
Loading