1
0

Integrate OIDC authentication with Authentik

Added `oidc-client-ts` and `jwt-decode` dependencies to support OIDC authentication. Implemented `authFetch` for secure API calls and integrated user session management with `AuthBootstrap`. Updated `App` to utilize token-based authentication flows with protected endpoints.
This commit is contained in:
Márcio Eric 2026-01-14 19:55:24 -03:00
parent 90b427c5b7
commit 3e28d115e1
7 changed files with 180 additions and 7 deletions

22
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^13.5.0",
"lucide-react": "^0.562.0",
"oidc-client-ts": "^3.1.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-scripts": "5.0.1",
@ -10949,6 +10950,15 @@
"node": ">=4.0"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -11757,6 +11767,18 @@
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"license": "MIT"
},
"node_modules/oidc-client-ts": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz",
"integrity": "sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==",
"license": "Apache-2.0",
"dependencies": {
"jwt-decode": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",

View File

@ -8,6 +8,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^13.5.0",
"oidc-client-ts": "^3.1.0",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",

View File

@ -1,6 +1,8 @@
import React, { useState, useEffect, useRef } from 'react';
import { Database, Globe, Download, Play, Pause, Trash2, Plus, RefreshCw, Activity, Clock, CircleX, CircleCheck, Grid, Calendar, Terminal } from 'lucide-react';
import * as signalR from '@microsoft/signalr';
import { authFetch } from './auth/authFetch';
import { getAccessToken } from './auth/userManager';
const API_BASE = 'http://localhost:5123';
@ -36,7 +38,9 @@ const WebScraper = () => {
// Conectar ao SignalR
useEffect(() => {
const connection = new signalR.HubConnectionBuilder()
.withUrl(`http://localhost:5123/ws/scrape`)
.withUrl(`${API_BASE}/ws/scrape`, {
accessTokenFactory: async () => (await getAccessToken()) ?? ''
})
.withAutomaticReconnect()
.build();
@ -208,7 +212,7 @@ const WebScraper = () => {
const loadSessions = async () => {
try {
const response = await fetch(`${API_BASE}/sessions`);
const response = await authFetch(`${API_BASE}/sessions`);
const data = await response.json();
setSessions(data);
} catch (err) {
@ -218,7 +222,7 @@ const WebScraper = () => {
const loadSessionStatus = async (sessionId) => {
try {
const response = await fetch(`${API_BASE}/sessions/${sessionId}/scrap/status`);
const response = await authFetch(`${API_BASE}/sessions/${sessionId}/scrap/status`);
const data = await response.json();
setSessionStatus(data);
} catch (err) {
@ -228,7 +232,7 @@ const WebScraper = () => {
const startScrapingById = async (sessionId, sessionName) => {
try {
await fetch(`${API_BASE}/sessions/${sessionId}/scrap/start`, {
await authFetch(`${API_BASE}/sessions/${sessionId}/scrap/start`, {
method: 'POST'
});
addLog(`Iniciando scraping da sessão: ${sessionName}`, 'info');
@ -239,7 +243,7 @@ const WebScraper = () => {
const stopScrapingById = async (sessionId, sessionName) => {
try {
await fetch(`${API_BASE}/sessions/${sessionId}/scrap/stop`, {
await authFetch(`${API_BASE}/sessions/${sessionId}/scrap/stop`, {
method: 'POST'
});
addLog(`Parando sessão: ${sessionName}`, 'warning');
@ -262,7 +266,7 @@ const WebScraper = () => {
if (!selectedSession || !newUrl) return;
try {
const response = await fetch(`${API_BASE}/sessions/${selectedSession.sessionId}/queue`, {
const response = await authFetch(`${API_BASE}/sessions/${selectedSession.sessionId}/queue`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: newUrl })

82
src/AuthBootstrap.js Normal file
View File

@ -0,0 +1,82 @@
import React, { useEffect, useState } from "react";
import { userManager } from "./auth/userManager";
/**
* Responsável por:
* - tratar /callback (retorno do Authentik)
* - garantir que o usuário esteja logado antes de renderizar o app
*/
export default function AuthBootstrap({ children }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
(async () => {
try {
const { pathname, search } = window.location;
if (pathname === "/callback") {
// Finaliza o fluxo OIDC
await userManager.signinRedirectCallback();
// Volta para home (mantém a URL limpa)
window.history.replaceState({}, document.title, "/");
setLoading(false);
return;
}
const user = await userManager.getUser();
if (!user || user.expired) {
await userManager.signinRedirect();
return;
}
setLoading(false);
} catch (e) {
setError(e?.message ?? String(e));
setLoading(false);
}
})();
}, []);
if (loading) {
return (
<div style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
}}>
Carregando autenticação...
</div>
);
}
if (error) {
return (
<div style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 24,
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
}}>
<div>
<h2 style={{ marginBottom: 12 }}>Erro na autenticação</h2>
<pre style={{ whiteSpace: "pre-wrap" }}>{error}</pre>
<button
onClick={() => userManager.signinRedirect()}
style={{ marginTop: 12, padding: "8px 12px" }}
>
Tentar novamente
</button>
</div>
</div>
);
}
return children;
}

24
src/auth/authFetch.js Normal file
View File

@ -0,0 +1,24 @@
import { getAccessToken, userManager } from "./userManager";
/**
* fetch() que injeta automaticamente Authorization: Bearer <token>
* Se não estiver logado, redireciona para o login.
*/
export async function authFetch(input, init = {}) {
const token = await getAccessToken();
if (!token) {
// Não está autenticado (ou token expirou)
await userManager.signinRedirect();
// A linha abaixo nunca deve acontecer (vai redirecionar), mas mantém o contrato
throw new Error("Not authenticated");
}
const headers = new Headers(init.headers || {});
headers.set("Authorization", `Bearer ${token}`);
return fetch(input, {
...init,
headers,
});
}

37
src/auth/userManager.js Normal file
View File

@ -0,0 +1,37 @@
import { UserManager, WebStorageStateStore, Log } from "oidc-client-ts";
// Opcional: habilite logs enquanto estiver configurando
// Log.setLogger(console);
// Log.setLevel(Log.DEBUG);
// Configure estes valores via .env (recomendado) ou altere aqui.
// CRA só expõe variáveis que começam com REACT_APP_
const AUTHENTIK_AUTHORITY =
process.env.REACT_APP_AUTHENTIK_AUTHORITY ||
"https://auth.seu-dominio.com/application/o/seu-app/";
const AUTHENTIK_CLIENT_ID =
process.env.REACT_APP_AUTHENTIK_CLIENT_ID || "SEU_CLIENT_ID";
export const userManager = new UserManager({
authority: AUTHENTIK_AUTHORITY,
client_id: AUTHENTIK_CLIENT_ID,
redirect_uri: `${window.location.origin}/callback`,
post_logout_redirect_uri: `${window.location.origin}/`,
response_type: "code",
scope: "openid profile email",
// Persistência
userStore: new WebStorageStateStore({ store: window.localStorage }),
stateStore: new WebStorageStateStore({ store: window.localStorage }),
// Em Authentik, normalmente funciona sem silent renew.
// Se quiser renovar automaticamente, você pode adicionar silent_redirect_uri
// e criar o public/silent-renew.html.
});
export async function getAccessToken() {
const user = await userManager.getUser();
if (!user || user.expired) return null;
return user.access_token;
}

View File

@ -2,10 +2,13 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import AuthBootstrap from './AuthBootstrap';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
// <React.StrictMode>
<AuthBootstrap>
<App />
</AuthBootstrap>
// </React.StrictMode>
);