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:
parent
90b427c5b7
commit
3e28d115e1
22
package-lock.json
generated
22
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
16
src/App.js
16
src/App.js
@ -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
82
src/AuthBootstrap.js
Normal 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
24
src/auth/authFetch.js
Normal 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
37
src/auth/userManager.js
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
<App />
|
||||
<AuthBootstrap>
|
||||
<App />
|
||||
</AuthBootstrap>
|
||||
// </React.StrictMode>
|
||||
);
|
||||
Loading…
Reference in New Issue
Block a user