feat(auth): replace cookie/api-key auth with JWT Bearer tokens, separate UI from API
Publish Metadata Agent Image / build-and-push-image (push) Successful in 6m3s
Publish Node Player Image / build-and-push-image (push) Failing after 58s
Publish Web Player Image / build-and-push-image (push) Has been cancelled

- Add JWT Bearer token validation to Rust API via OIDC provider JWKS
  with automatic key rotation and 1-hour cache
- Remove x-api-key auth support and built-in web UI from furumi-web-player,
  leaving it as a pure API server
- Add /auth/token endpoint to Node player server to expose OIDC access
  tokens to the frontend
- Move Node player auth endpoints from /api/* to /auth/* to avoid
  path conflicts with Rust API
- Add static file serving to Node Express server for production
  single-container deployment
- Fix SameSite=Strict cookie issue breaking OIDC redirect flow (use Lax)
- Add Dockerfile.node-player with multi-stage Node.js build
- Add CI workflows for node-player Docker image (dev + release)
- Optimize Rust Dockerfiles with dependency caching layer
- Update docker-compose with OIDC env vars and OLLAMA_MODEL support
- Cherry-pick agent LLM client fixes from DEV branch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ultradesu
2026-04-08 14:51:52 +01:00
parent 94d14e8fc8
commit e99cacae8b
20 changed files with 515 additions and 161 deletions
+37 -5
View File
@@ -1,5 +1,6 @@
import 'dotenv/config';
import path from 'path';
import cors from 'cors';
import express from 'express';
import { auth } from 'express-openid-connect';
@@ -28,7 +29,6 @@ const oidcConfig = {
};
if (!disableAuth && (!oidcConfig.clientID || !oidcConfig.issuerBaseURL || !oidcConfig.clientSecret)) {
// Keep a clear startup failure if OIDC is not configured.
throw new Error(
'OIDC config is missing. Set OIDC_ISSUER_BASE_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET in server/.env (or set DISABLE_AUTH=true)',
);
@@ -46,11 +46,11 @@ if (!disableAuth) {
app.use(auth(oidcConfig));
}
app.get('/api/health', (_req, res) => {
app.get('/auth/health', (_req, res) => {
res.json({ ok: true });
});
app.get('/api/me', (req, res) => {
app.get('/auth/me', (req, res) => {
if (disableAuth) {
res.json({
authenticated: false,
@@ -74,7 +74,32 @@ app.get('/api/me', (req, res) => {
});
});
app.get('/api/login', (req, res) => {
app.get('/auth/token', (req, res) => {
if (disableAuth) {
res.status(204).end();
return;
}
if (!req.oidc.isAuthenticated()) {
res.status(401).json({ authenticated: false });
return;
}
const accessToken = req.oidc.accessToken?.access_token;
const expiresAt = req.oidc.accessToken?.expires_at;
if (!accessToken) {
res.status(500).json({ error: 'no access token in session' });
return;
}
res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_at: expiresAt,
});
});
app.get('/auth/login', (req, res) => {
if (disableAuth) {
res.status(204).end();
return;
@@ -85,7 +110,7 @@ app.get('/api/login', (req, res) => {
});
});
app.get('/api/logout', (req, res) => {
app.get('/auth/logout', (req, res) => {
if (disableAuth) {
res.status(204).end();
return;
@@ -96,6 +121,13 @@ app.get('/api/logout', (req, res) => {
});
});
// Production: serve Vite-built client as static files
const clientDist = path.resolve(import.meta.dirname, '../../client/dist');
app.use(express.static(clientDist));
app.get('*', (_req, res) => {
res.sendFile(path.join(clientDist, 'index.html'));
});
app.listen(port, () => {
console.log(
`${disableAuth ? 'NO-AUTH' : 'OIDC auth'} server listening on http://localhost:${port}`,