195 lines
5.1 KiB
TeX
195 lines
5.1 KiB
TeX
\section{JWT}
|
|
|
|
\begin{frame}[shrink=15]{Mi az a JWT?}
|
|
\begin{block}{JSON Web Token (RFC 7519)}
|
|
Kompakt, önálló token információ biztonságos továbbításához.
|
|
\end{block}
|
|
\begin{itemize}
|
|
\item Digitálisan aláírt, Base64URL kódolt
|
|
\item Használat: authentikáció, stateless session
|
|
\item \textbf{NEM} titkosított - payload olvasható!
|
|
\end{itemize}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=15]{JWT formátum}
|
|
\texttt{\textcolor{red}{HEADER}.\textcolor{purple}{PAYLOAD}.\textcolor{blue}{SIGNATURE}}
|
|
\begin{tiny}
|
|
\begin{verbatim}
|
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZ
|
|
SI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36P
|
|
\end{verbatim}
|
|
\end{tiny}
|
|
\begin{itemize}
|
|
\item \textcolor{red}{Header}: Algoritmus + token típus
|
|
\item \textcolor{purple}{Payload}: Claims (user adatok)
|
|
\item \textcolor{blue}{Signature}: Integritás védelem
|
|
\end{itemize}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=15]{JWT Header \& Payload}
|
|
\begin{columns}
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{Header:}
|
|
\begin{verbatim}
|
|
{
|
|
"alg": "HS256",
|
|
"typ": "JWT"
|
|
}
|
|
\end{verbatim}
|
|
\end{column}
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{Payload:}
|
|
\begin{verbatim}
|
|
{
|
|
"sub": "1234",
|
|
"name": "John",
|
|
"iat": 1516239022,
|
|
"exp": 1516242622
|
|
}
|
|
\end{verbatim}
|
|
\end{column}
|
|
\end{columns}
|
|
\begin{alertblock}{Fontos}
|
|
Ne tároljunk jelszót payload-ban!
|
|
\end{alertblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=15]{JWT Signature}
|
|
\begin{verbatim}
|
|
HMACSHA256(
|
|
base64(header) + "." + base64(payload),
|
|
secret
|
|
)
|
|
\end{verbatim}
|
|
\begin{columns}
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{HMAC:} Egy kulcs, gyors (HS256)
|
|
\end{column}
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{RSA:} Privát+publikus, biztonságosabb (RS256)
|
|
\end{column}
|
|
\end{columns}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=20]{JWT generálás Node.js}
|
|
\begin{verbatim}
|
|
const jwt = require('jsonwebtoken');
|
|
|
|
const payload = { userId: user.id, role: user.role };
|
|
|
|
const token = jwt.sign(
|
|
payload,
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: '1h' }
|
|
);
|
|
|
|
res.json({ token });
|
|
\end{verbatim}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=20]{JWT validálás Node.js}
|
|
\begin{verbatim}
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
|
|
if (!token) return res.status(401).json({ error: 'No token' });
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
req.user = decoded;
|
|
next();
|
|
} catch (err) {
|
|
res.status(403).json({ error: 'Invalid' });
|
|
}
|
|
\end{verbatim}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=20]{JWT middleware}
|
|
\begin{verbatim}
|
|
const authJWT = (req, res, next) => {
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
if (!token) return res.status(401).send('Token required');
|
|
|
|
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
|
|
if (err) return res.status(403).send('Invalid');
|
|
req.user = user;
|
|
next();
|
|
});
|
|
};
|
|
|
|
app.get('/protected', authJWT, (req, res) => {
|
|
res.json({ data: 'Secret', user: req.user });
|
|
});
|
|
\end{verbatim}
|
|
\end{frame}
|
|
|
|
\begin{frame}[shrink=15]{JWT vs Session}
|
|
\begin{columns}
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{JWT:}
|
|
\begin{itemize}
|
|
\item Stateless
|
|
\item Skálázható
|
|
\item Cross-domain
|
|
\item Nehéz visszavonni
|
|
\item Token méret
|
|
\end{itemize}
|
|
\end{column}
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{Session:}
|
|
\begin{itemize}
|
|
\item Stateful
|
|
\item Szerver tárolja
|
|
\item Könnyű visszavonás
|
|
\item Nehezebb skálázás
|
|
\item Kis cookie
|
|
\end{itemize}
|
|
\end{column}
|
|
\end{columns}
|
|
\end{frame}
|
|
|
|
\begin{frame}[shrink=15]{Access \& Refresh Token}
|
|
\begin{block}{Két token stratégia}
|
|
\textbf{Access token}: Rövid élet (15 perc), API hozzáférés\\
|
|
\textbf{Refresh token}: Hosszú élet (7 nap), új access token generálás
|
|
\end{block}
|
|
\begin{enumerate}
|
|
\item Login $\to$ Access + Refresh token
|
|
\item API kérés Access token-nel
|
|
\item Access lejár $\to$ Refresh-el új Access-t kér
|
|
\item Refresh lejár $\to$ Újra login
|
|
\end{enumerate}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=20]{Refresh Token példa}
|
|
\begin{verbatim}
|
|
app.post('/refresh', (req, res) => {
|
|
const { refreshToken } = req.body;
|
|
if (!refreshToken) return res.sendStatus(401);
|
|
|
|
jwt.verify(refreshToken, REFRESH_SECRET, (err, user) => {
|
|
if (err) return res.sendStatus(403);
|
|
|
|
const accessToken = jwt.sign(
|
|
{ userId: user.userId },
|
|
ACCESS_SECRET,
|
|
{ expiresIn: '15m' }
|
|
);
|
|
res.json({ accessToken });
|
|
});
|
|
});
|
|
\end{verbatim}
|
|
\end{frame}
|
|
|
|
\begin{frame}[shrink=15]{JWT Security Best Practices}
|
|
\begin{enumerate}
|
|
\item Rövid lejárat (15-60 perc)
|
|
\item Erős secret kulcs (min 256 bit)
|
|
\item HTTPS kötelező
|
|
\item Ne tároljunk érzékeny adatot payload-ban
|
|
\item Validáld mindig az \texttt{exp} és \texttt{iat} claim-eket
|
|
\item Használj RS256-ot HS256 helyett production-ben
|
|
\item Refresh token rotation
|
|
\item Token blacklist megfontolása
|
|
\end{enumerate}
|
|
\end{frame}
|