5. JSON Web Tokens
5. JSON Web Tokens
In deze les bespreken we hoe we met behulp van JSON Web Tokens (JWT) een stateless authenticatiesysteem kunnen implementeren voor onze API. We vergelijken deze methode met sessiegebaseerde authenticatie en gaan dieper in op de voor- en nadelen van beide benaderingen. Daarnaast leren we werken met asymmetrische sleutels voor het aanmaken en valideren van JWT's.
We kijken ook naar de inzet van JWT's in gedistribueerde systemen, zoals microservices. Stateless authenticatie speelt namelijk een cruciale rol in de schaalbaarheid en loadbalancing van applicaties in zulke omgevingen.
Stateful vs Stateless
Stateful
In een stateful systeem wordt de status van een gebruiker of diens sessie tussen aanvragen door bijgehouden. Dit gebeurt vaak met een cryptografisch gegenereerde sessietoken die aan een gebruiker is gekoppeld. Deze token wordt meestal opgeslagen in een cookie aan de clientzijde (bijvoorbeeld in een webbrowser) en dient om zowel de identiteit van de gebruiker te verifiëren als om gekoppelde gegevens, zoals winkelmandjes of gebruikersvoorkeuren, te bewaren.
Dit type systeem is vooral nuttig voor applicaties waarin constante interactie en state management nodig zijn, zoals server-side applicaties gebouwd met traditionele technologieën (bijvoorbeeld PHP). Het biedt voordelen zoals het volgen van gebruikersactiviteit en het mogelijk maken van personalisatie zonder dat gebruikers telkens opnieuw hoeven in te loggen.
Toch kent deze methode ook beperkingen, met name in moderne webapplicaties waar frontend en backend vaak los van elkaar opereren. Stateful systemen kunnen de schaalbaarheid van een applicatie bemoeilijken, vooral wanneer sessie-informatie in het servergeheugen wordt opgeslagen. Dit verhoogt de behoefte aan servercapaciteit en vereist vaak een gedeelde database die voortdurend moet worden aangesproken. Dit kan leiden tot vertragingen en verhoogde complexiteit, vooral bij applicaties die een groot aantal gelijktijdige gebruikers moeten ondersteunen.
Voor moderne toepassingen met hoge eisen aan schaalbaarheid en prestaties biedt een stateless aanpak vaak een efficiënter alternatief.
Stateless
In een stateless systeem wordt de status van een gebruiker of diens sessie niet tussen aanvragen door bijgehouden. In plaats daarvan wordt een cryptografisch gegenereerde token gebruikt om de identiteit van de gebruiker te verifiëren. Deze token kan door de client worden bijgehouden in een cookie (voor extra veiligheid) of op een andere manier, zoals in session storage, in memory (state), of in local storage. Als deze niet in een cookie wordt opgeslagen, is het aan de client om het token bij elke aanvraag via een header mee te sturen (meer hierover later).
In een stateless token bevindt zich, naast cryptografische controle, ook data die door deze controle geverifieerd kan worden met de sleutel die de server heeft. Dit kan bijvoorbeeld de gebruikersrol, het gebruikers-ID of de gebruikersnaam zijn, en deze gegevens kunnen met zekerheid worden geverifieerd door een autoritatieve server via een sleutel of secret. Dit vermindert de noodzaak voor een database en verhoogt de schaalbaarheid van de applicatie, omdat de sessietoken niet steeds opgezocht hoeft te worden.
Stateless applicaties zijn beter geschikt voor schaalbaarheid en flexibiliteit, maar brengen ook nieuwe complexiteiten met zich mee. Aangezien er geen state tussen aanvragen mag worden bijgehouden, moet je andere manieren vinden om de benodigde data beschikbaar te maken. Deze complexiteit wordt vaak naar de frontend verplaatst, waar deze data wordt bijgehouden voordat deze naar de backend wordt verzonden.
Verder is het, bij een stateless systeem, moeilijker om een gebruiker uit te loggen op alle toestellen. Daarnaast kan het even duren (maximaal de levensduur van een JWT) voordat een uitgelogde gebruiker echt uitgelogd is, ook dingen zoals het aanpassen van de rechten van een gebruiker worden met vertraging doorgevoerd.
Voorbeeld: Een winkelmandje dat in sessiedata wordt bijgehouden op de backend (stateful), wordt nu op de client bijgehouden in de state, bijvoorbeeld met behulp van React Redux of andere state management tools voor de frontend (stateless).
In een stateless systeem hoeft de authenticatie ook niet te worden uitgevoerd door dezelfde server als de API of applicatie. Dit maakt het eenvoudiger om te werken met externe authenticatiediensten zoals Auth0, AWS Cognito, Azure AD, enzovoort.
Een mogelijke authenticatieflow ziet er als volgt uit, waarbij je inlogt bij een authenticatieserver:
Vervolgens kan je de JWT gebruiken om de API of Applicatie aan te spreken:
In bovenstaande gebruiken we de Auth Server nog na de authenticatie echter hadden we hier ook gebruikt kunnen maken van een publieke key (later hier meer over) om de verificatie van de JWT uit te voeren (de Validate JWT stap in bovenstaande figuur), dit ziet er dan als volgt uit en maakt de auth server na de initiële authenticate zo goed als overbodig (in een versimpelde flow):
Crash Course Scaling
In deze demo laten we het verschil zien tussen een sessiegebaseerde aanpak en het gebruik van een JWT-token. Het eerste systeem werkt met een traditionele sessie, waarbij sessie-informatie in het geheugen van de server wordt opgeslagen. Het tweede systeem maakt gebruik van een stateless JWT-token, waarbij gebruikersinformatie in het token zelf wordt opgeslagen. Beide systemen draaien achter een load balancer die het verkeer verdeelt over meerdere servers. Dit toont aan hoe sessies afhankelijk zijn van server-side opslag en sticky sessions, terwijl JWT-tokens schaalbaarder zijn in een gedistribueerde omgeving omdat ze onafhankelijk van een specifieke server functioneren.
Wat vooral duidelijk zou moeten zijn, is dat de stateless approach ons in staat stelt om de gebruiker zonder enig extra's verkeer te sturen naar welke server we ook willen en de verwachte data terug te krijgen. Dit omdat we niet verwachten dat de server op de hoogte is van voorafgaande data (lees: state), zoals sessies.
Dit wordt nog belangrijker bij het werken met microservices, waarbij elke microservice verantwoordelijk is voor een specifiek deel van de applicatie:
In de onderstaande video wordt gebruikgemaakt van sessiegebaseerde authenticatie, waarbij een unieke sessie-ID wordt gegenereerd. Deze wordt opgeslagen in het geheugen van de server zelf. Zoals je zult zien, kan dit problematisch zijn in een omgeving met meerdere servers. Niet elke server heeft immers toegang tot de sessiegegevens; alleen de server waarop de server action is uitgevoerd, kan de sessiedata teruggeven.
Dit probleem kan op verschillende manieren worden opgelost. Zo zou je bijvoorbeeld gebruik kunnen maken van
sticky sessions(mits je niet met een microservicearchitectuur werkt), waarbij elke client telkens naar dezelfde server wordt gestuurd. Een andere optie is het opslaan van sessies in een gedeelde database, zodat elke server toegang heeft tot dezelfde sessiegegevens. Dit kan echter nadelig zijn voor de prestaties en is minder geschikt voor een microservicearchitectuur.Het gebruik van sessiegebaseerde authenticatie zonder JWT is dus niet per se beter of slechter, maar het lost wel enkele standaardproblemen op. Dit houdt uiteraard geen rekening met complexere scenario’s, zoals het ongeldig maken van tokens vóór hun vervaldatum, maar dat valt buiten de scope van dit hoofdstuk.
In de onderstaande video wordt gebruikgemaakt van JWT-gebaseerde authenticatie. Hierbij wordt een JWT-token gegenereerd en aan de client teruggegeven. Elke server kan vervolgens dit JWT-token valideren, waardoor alle servers de benodigde gegevens uit het token kunnen ophalen en teruggeven. Voor dit proces is geen extra configuratie of gedeelde database nodig (dit geldt uiteraard alleen voor autorisatie; voor andere data blijft een database noodzakelijk).
Of de ene methode beter is dan de andere, hangt sterk af van je use case en de complexiteit van je applicatie. Het is daarom belangrijk om je applicatie te testen en te evalueren om te bepalen welke methode het beste past. Sessies zijn relatief snel en eenvoudig te implementeren, terwijl een volledige JWT-oplossing meer complexiteit met zich meebrengt, zeker als je aspecten zoals refreshtokens en token-invalidatie wilt meenemen.
JSON Web Tokens Basics
Een JWT (JSON Web Token) bestaat uit drie delen, die elk zijn gescheiden door een punt (.):
Header: Dit bevat metadata over de token, zoals het type token (meestal JWT) en het algoritme dat is gebruikt om de token te ondertekenen (bijvoorbeeld
HS256voor HMAC SHA-256 (symmetrisch) ofRS256voor RSA SHA-256 (asymmetrisch)).Payload: Dit bevat de daadwerkelijke claims of informatie over de gebruiker of sessie. In de payload kunnen standaard claims staan (zoals
subvoor subject,expvoor expiration) en custom claims die de applicatie zelf definieert. De payload bevat al de applicatie specifieke data die je wilt verzenden, zoals de gebruikers-ID of rollen.Signature: Dit is de handtekening die ervoor zorgt dat de token niet kan worden aangepast. De handtekening wordt gegenereerd door de header en payload te combineren en deze met een geheime sleutel te versleutelen (bij symmetrische encryptie) of met een private key (bij asymmetrische encryptie). Bij het valideren van de token controleert de server of de handtekening overeenkomt met de ontvangen header en payload.
Samengevat ziet een JWT er als volgt uit:
header.payload.signatureOf meer concreet:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1pdGNoIE1vbW1lcnMiLCJyb2xlIjoidGVhY2hlciIsImVtYWlsIjoibWl0Y2gubW9tbWVyc0B0aG9tYXNtb3JlLmJlIiwiaWF0IjoxNTE2MjM5MDIyfQ.1JDEpnLecpPACHmoUWZ_a6Q3DaAZDYBre7exM9Emd7EAls je naar de website jwt.io gaat en bovenstaande token hier invoert, zie je dat deze token gegenereerd is met de volgende claims:
{
"sub": "1234567890",
"name": "Mitch Mommers",
"role": "teacher",
"email": "mitch.mommers@thomasmore.be",
"iat": 1516239022
}En dat deze niet valide is... Dit komt doordat de token is gemaakt met een secret: thomasmore20242025secret. Zodra je deze invult in het vakje bij signature, zal hij aangeven Valid secret. Deze token heeft geen expiration (exp) claim en vervalt dus nooit.
Deze structuur zorgt ervoor dat de integriteit van de gegevens in de token gewaarborgd blijft en maakt het mogelijk om de token stateless en veilig in te zetten voor authenticatie en autorisatie.
Notitie
Het is belangrijk om te beseffen dat de inhoud van je token altijd leesbaar is en dus niet gebruikt kan worden om gevoelige data te delen. JWT zorgt er enkel voor dat je token cryptografisch geverifieerd kan worden tegen aanpassingen, niet dat niemand de inhoud van de token kan zien.
Gereserveerde Claims
Een JWT heeft een aantal gereserveerde claims (gedefinieerd in RFC 7519) die een specifieke functie hebben, daarnaast kan je toevoegen wat je ook maar wilt (zoals in het voorbeeld hierboven):
- iss (issuer): Uitgever van de JWT
- sub (subject): Onderwerp van de JWT (de gebruiker)
- aud (audience): Ontvanger voor wie de JWT bedoeld is
- exp (expiration time): Tijdstip waarop de JWT verloopt
- nbf (not before time): Tijdstip vóór welke de JWT niet geaccepteerd mag worden voor verwerking
- iat (issued at time): Tijdstip waarop de JWT is uitgegeven; kan worden gebruikt om de ouderdom van de JWT te bepalen
- jti (JWT ID): Unieke identificatie; kan worden gebruikt om te voorkomen dat de JWT opnieuw wordt gebruikt (zorgt ervoor dat een token slechts één keer gebruikt kan worden)
Wil je dus dat je token na 1 uur vervalt dan voeg je aan je token de claim exp toe met een absolute waarde bijvoorbeeld 1675276400. Dan zal elke implementatie die een JWT valideert de token moeten weigeren om dat deze expired is. Zo kan je zelfs zonder database en sessies een authenticatie met en JWT token doen vervallen.
Voor meer informatie:
In de praktijk
We voegen JWT-authenticatie toe voor de API-routes, in de oefeningen vervang je de session id authenticatie met een JWT die in de proxy uitgelezen kan worden zonder de database te raadplegen.
Startbestanden
Info
Normaal gesproken, wanneer we met JWT-tokens werken, maken we ook gebruik van een refreshtoken. Deze kan worden gebruikt om een nieuwe JWT-token aan te vragen, zodat we een short-lived JWT-token kunnen gebruiken. Voor onze cursus valt dit echter buiten de scope, maar het is belangrijk om dit in je achterhoofd te houden.
Voor nu zal je na 24 uur opnieuw moeten inloggen, omdat we de token dan laten vervallen.
Aanpassen van de login
We beginnen met het installeren van de library jsonwebtoken. Dit is zo goed als de de facto standaard voor JWT binnen Node.js.
De startbestanden bevatten reeds een route (POST: /api/auth) waarmee de gebruiker kan inloggen. Het endpoint verwacht een JSON-object met het email-adres en wachtwoord van de gebruiker als argument en geeft de JWT terug als een geldige email/password combinatie ingestuurd is.
{
"email": "<email>",
"password": "<password>"
}{
"token": "<token>"
}De route is grotendeels af. De code is gebaseerd op de signInAction, maar stelt geen session-cookie in en moet nog een JWT teruggeven.
Hiervoor schrijven we een kleine utility voor het maken van tokens. Deze utility breiden we later uit, zodat we hier ook asymmetrische sleutels voor kunnen gebruiken. Voorlopig maken we gebruik van een simpele symmetrische sleutel die per definitie onveilig is. Deze sleutel wordt in de .env-file geplaatst.
JWT_SECRET=ONVEILIG_jwt_secretimport 'server-only'
import JWT from 'jsonwebtoken'
import type {Profile} from '@/models/users'
import type {Role} from '@/generated/prisma/enums'
const {JWT_SECRET} = process.env
const TOKEN_EXPIRATION = '24h'
export interface TokenBody {
email: string
id: string
role: Role
username: string
iat: number
exp: number
iss: string
sub: string
}
export const createJwtToken = (user: Profile) => {
return JWT.sign(
{
email: user.email,
id: user.id,
username: user.username,
role: user.role,
},
JWT_SECRET as string,
{expiresIn: TOKEN_EXPIRATION, subject: user.email, issuer: 'contacts-app'},
)
}import type {NextRequest, NextResponse} from 'next/server'
import {getUserByEmail} from '@/dal/users'
import {getLogger} from '@/lib/logger'
import {getSalt, hashOptions, verifyPassword} from '@/lib/passwordUtils'
import {ok, unauthorized} from '@/lib/routeResponses'
import {createJwtToken} from '@/lib/jwtUtils'
export async function POST(request: NextRequest): Promise<NextResponse> {
const {email, password} = (await request.json()) as {email: string; password: string}
const user = await getUserByEmail(email)
const logger = await getLogger()
const timingSafePassword = `${hashOptions.iterations}$${hashOptions.keyLength}$preventTimingBasedAttacks123$${getSalt()}`
const isValidPassword = verifyPassword(user?.password ?? timingSafePassword, password)
if (!isValidPassword) {
logger.warn(`Failed sign in attempt for ${email}.`)
return unauthorized()
}
logger.info(`Successful authentication request for ${user!.id}`)
const token = createJwtToken(user!)
return ok({token})
}De POST-route is nu volledig geïmplementeerd en zal bij een geldige login een token teruggeven.
Waarschuwing
Dit is nog altijd geen cursus over cryptografie en beveiliging. Idealiter gebruik je een extern loginsysteem of laat je dit uitgebreid controleren door een expert in softwarebeveiliging.
Ter illustratie is dit voldoende, maar een veilig systeem moet uitgebreid getest worden en voorzien zijn van de nodige best practices. Helaas valt dit buiten de scope van onze cursus.
Endpoints beveiligen
We hebben nu een login-endpoint gemaakt en een token teruggekregen. Deze token kunnen we nu gebruiken om onze andere endpoints te beveiligen.
We beginnen opnieuw met het schrijven van een kleine utility functie, dit keer valideert de utility de ingezonden token. Om de token in te zenden moet de Authorization header toegevoegd worden aan het request. De waarde van de header heeft volgende structuur: "Bearer TOKEN"
import 'server-only'
import JWT from 'jsonwebtoken'
import type {Profile} from '@/models/users'
import type {Role} from '@/generated/prisma/enums'
const {JWT_SECRET} = process.env
const TOKEN_EXPIRATION = '24h'
export interface TokenBody {
email: string
id: string
role: Role
username: string
iat: number
exp: number
iss: string
sub: string
}
export const validateJwtToken = (token: string) => {
return JWT.verify(token, JWT_SECRET as string) as unknown as TokenBody
}
export const createJwtToken = (user: Profile) => {
return JWT.sign(
{
email: user.email,
id: user.id,
username: user.username,
role: user.role,
},
JWT_SECRET as string,
{expiresIn: TOKEN_EXPIRATION, subject: user.email, issuer: 'contacts-app'},
)
}import type {NextRequest, NextResponse} from 'next/server'
import type {CreateContactParams} from '@/dal/contacts'
import {createContact, getContacts} from '@/dal/contacts'
import {created, ok, unauthorized} from '@/lib/routeResponses'
import {validateJwtToken} from '@/lib/jwtUtils'
export async function GET(request: NextRequest): Promise<NextResponse> {
const [_, token] = (request.headers.get('Authorization') || ' ').split(' ')
const tokenBody = validateJwtToken(token)
if (!tokenBody) return unauthorized()
const contactName = request.nextUrl.searchParams.get('name') ?? ''
const contacts = await getContacts(tokenBody.id, contactName)
return ok(contacts)
}
export async function POST(request: NextRequest): Promise<NextResponse> {
const [_, token] = (request.headers.get('Authorization') || ' ').split(' ')
const tokenBody = validateJwtToken(token)
if (!tokenBody) return unauthorized()
const data = (await request.json()) as CreateContactParams
const newContact = await createContact({...data, userId: tokenBody.id})
return created(newContact)
}import type {NextRequest, NextResponse} from 'next/server'
import type {UpdateContactParams} from '@/dal/contacts'
import {deleteContact} from '@/dal/contacts'
import {getContact, updateContact} from '@/dal/contacts'
import {ok, unauthorized} from '@/lib/routeResponses'
import {validateJwtToken} from '@/lib/jwtUtils'
interface RouteParams {
params: Promise<{contactId: string}>
}
export async function GET(request: NextRequest, {params}: RouteParams): Promise<NextResponse> {
const [_, token] = (request.headers.get('Authorization') || ' ').split(' ')
const tokenBody = validateJwtToken(token)
if (!tokenBody) return unauthorized()
const {contactId} = await params
const contact = await getContact(contactId, tokenBody.id)
return ok(contact)
}
export async function PUT(request: NextRequest, {params}: RouteParams): Promise<NextResponse> {
const [_, token] = (request.headers.get('Authorization') || ' ').split(' ')
const tokenBody = validateJwtToken(token)
if (!tokenBody) return unauthorized()
const {contactId} = await params
const data = (await request.json()) as UpdateContactParams
const updatedContact = await updateContact({...data, userId: tokenBody.id, id: contactId})
return ok(updatedContact)
}
export async function DELETE(request: NextRequest, {params}: RouteParams): Promise<NextResponse> {
const [_, token] = (request.headers.get('Authorization') || ' ').split(' ')
const tokenBody = validateJwtToken(token)
if (!tokenBody) return unauthorized()
const {contactId} = await params
await deleteContact(contactId, tokenBody.id)
return ok()
}Notitie
Bovenstaande code bevat nog heel wat boilerplate code. In het laatste hoofdstuk worden wrapper functies besproken waarmee deze repetitieve code afgezonderd kan worden (voor server functions en route handlers).
CORS Headers
Bovenstaande code werkt in Postman, maar kan nog niet aangesproken worden vanuit een website met een andere origin. Om dit probleem op te lossen, moeten we de CORS headers nog aanpassen.
import type {NextRequest, NextResponse} from 'next/server'
export function corsProxy(request: NextRequest, response: NextResponse): NextResponse {
if (!request.nextUrl.pathname.startsWith('/api')) return response
response.headers.set('Access-Control-Allow-Origin', '*')
if (request.method === 'OPTIONS') {
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
}
return response
}Asymmetrische Keys
In de laatste stap gaan we kijken naar het toepassen van asymmetrische sleutels in plaats van een geheime string. Dit is namelijk veiliger (veel moeilijker te kraken) en heeft als voordeel dat we de publieke sleutel kunnen delen met anderen. Hierdoor kunnen zij onze tokens valideren zonder dat ze de geheime string of private sleutel hoeven te kennen. Met andere woorden, ze kunnen de tokens wel valideren, maar niet zelf maken.
Keys genereren
Om asymmetrische sleutels te genereren kun je de volgende commando's gebruiken:
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -outform PEM -pubout -out public.pemTips
Deze commando's werken in GIT Bash op Windows: installatie GIT Bash
Dit zal twee bestanden aanmaken: private.pem (die je te allen tijde veilig moet bijhouden) en public.pem. De public.pem kun je delen met anderen die jouw tokens moeten valideren (bijvoorbeeld een client of mobiele app).
Belangrijk
Normaal gesproken sla je private.pem op een veilige locatie op, en public.pem op een veilige, gedeelde locatie. Het opslaan van deze sleutels in je .env-bestand brengt risico's met zich mee. Idealiter gebruik je een key vault of een speciale dienst om deze veilig te bewaren.
Omdat de sleutels meerdere lijnen bevatten, kunnen we de inhoud niet zomaar kopiëren naar de .env file. Dit probleem lossen we op door de keys te converteren een one-line string die in base64 gecodeerd wordt. Hierdoor blijft de structuur van de sleutel behouden, maar kunnen we ze eenvoudig als environment variables beheren. Dit is ook handig als de applicatie gedeployed wordt naar services zoals Vercel, Netlify, of Railway.
Onderstaande commando's converteren respectievelijk de private en public key naar een base64 string.
# De -w 0 parameter zorg ervoor dat alles op één lijn geplaatst wordt.
cat private.pem | base64 -w 0
cat public.pem | base64 -w 0Keys gebruiken
Vervolgens gebruiken we deze nieuwe environment variables om de tokens aan te maken en te verifiëren. Let op dat de juiste key gebruikt wordt voor aanmaken (private) en verifiëren (public).
Om de keys uit te lezen moet de base64 tekst terug omgevormd worden naar een gewone string in utf-8 formaat.
Merk op dat we, bij het aanmaken van een JWT, nu het algoritme moeten meegeven dat gebruikt werd om de keys aan te maken. De verificatiefunctie heeft dit algoritme nodig om de handtekening correct te kunnen valideren.
import 'server-only'
import JWT from 'jsonwebtoken'
import type {Profile} from '@/models/users'
import type {Role} from '@/generated/prisma/enums'
const {PUBLIC_KEY, PRIVATE_KEY} = process.env
const TOKEN_EXPIRATION = '24h'
const PUBLIC_KEY_DECODED = Buffer.from(PUBLIC_KEY!, 'base64').toString('utf-8')
const PRIVATE_KEY_DECODED = Buffer.from(PRIVATE_KEY!, 'base64').toString('utf-8')
export interface TokenBody {
email: string
id: string
role: Role
username: string
iat: number
exp: number
iss: string
sub: string
}
export const validateJwtToken = (token: string) => {
return JWT.verify(token, PUBLIC_KEY_DECODED) as unknown as TokenBody
}
export const createJwtToken = (user: Profile) => {
return JWT.sign(
{
email: user.email,
id: user.id,
username: user.username,
role: user.role,
},
PRIVATE_KEY_DECODED,
{
algorithm: 'RS256',
expiresIn: TOKEN_EXPIRATION,
subject: user.email,
issuer: 'contacts-app'
},
)
}PRIVATE_KEY=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Mwd2dna3BBZ0VBQW9JQ0FRQzFFaS81RjI1cENSOGgKUjErcUcyMDlwSXdmOHo0WUd2MEhHZjRRbVBDT3BXaFpyeWZ6endQUEhKenJVMURta3RxV09LczdUcFFEMGtGaQpWd2ZzT2F3QURPRzdGeUMrczB2R3p6cXcvMXZJcjF4WTJTRjRBRFMxVkZRTzZrWjRhZ0YybERUUDhLZDdVSEE4Ckcva255aUNndGZVRWlKVnQ4T3M4RGkraFBBQ2dOMkVVdUJQTXdnTUIyYnZqVEFDaDBlQ1hrVGRUdWNBYXRmZHUKeitSWk81SHVZZE03emZWa3dvVmE0b0Z0M0JEVGYvN0xnS3Q1blFlR2YvK2hMREVPZU53bVZkYVpySUNDcVAveAozc21VMUgrd3p5L3h6d3NUNm9NT2JKb2JsNExWNmVXc0FzWGQ4WCtRNXl6QXo4SmdHUzV0K3A0VlZJVEVPNVRtCmFWV2tyZXlGOEs4enpPYmVxampUWGxYQWRtenNmeDNyWnpWeG12bk01ZGIyaUhldTBvdWQ1ZjFWL2lUTVdnQUQKc1lsekJCcjRyRHNPZ3JuRExBTUtIMVI2V01IL29jQk9oQTFRUlpCei9WL2laL0hTMTZ3OWJ0Qjl4QktKVmFUSwpkWE5NcWN3NytqejRiVGFKMVlTS2twNjZwUlBhS0hkQjNhMklRdG40bzFRRXY3NlZtYnk4YUwzblFtRE4xQXVSCjhlU1J6Z29ZVHhiMjQwSmJoMlhMQWhaT1IxTDZBWjJBTkFqOXBhTEdOMGZseVZjbVZJc25XTWlPSnRvVkVZQjYKSHhtdGd1QUpUZjA2cXBLMGVvaUpjc3drT1hyUFlzem16R3ErMU16WVN5c085R044bjAxSWFQZWZ1SUhQOVJTWQpXbFRBTjc0VTNvanpUNFlBbFFGVzRnQ2FRY2NiclFJREFRQUJBb0lDQUNCSmUvQm9zekZiaFFlRUdKVFdpeVlnCnZ6TzdSZWs2ZHIvYmJLZzgvVGdTV29jVGFvWlk5WTdxeFRoWkJPWDRiYTIzc1NERUpEU0NPMUp6dmwvcThMYVoKbFc2czh4UE9HTWI4a0xTYTlCelcyQnFuclFZZTAwYzZJRHNBbjB3OWFzRmdNQUtjSU9FbU9MMGZKOUJtaEdiagp5eXJoeC9Od3NDRm5UaXJyN2hpamgxWVFkTWRnR1BVdGhBbEZ0aDhrczIyVFlWQTJvUUNpVGRxZWkxTTJBaEdGCnRBckg4T2ZrbjY3VEVNMHNIbnY0S05EVXdyN090MWJPWUNDM0pwQTBFTjF0RlBVV0ExeEpFa3BvdFgwcVh4S0IKLzBTWTdKckNpRmY4R1lhdC81QnhRcGpuYmF6anNTRi9kMFZ5Um5Db1ROVWppU2k0aDJ3T1M4RENLNWxQd2dyYQpnS2YwUFdjUTQ4Q05YZGxEQmN3b2J1NGRnZjRGcWtqNG9WaHpVUzAvL0FLZ2JMc2dIUm9wdWpxMFVOcTZLMjlmCjlFVzZPNExDdWd5b2dZb3R1cmU2TDFIa09GRjN1SFZhaWFGellKTkM3enNhazMxQWhyNjBUN1ZYS2xQbE9SV1IKcklyOHlRY3Q1d2x2Y0RDUW0ydDFiajI0ZjVkMHljZ2tNL29CVVNTWHYvYzVkeVd5d2dMS3pORjV3MEloVFNxawp6cENmZUFuL25nR3AwVzNnUjBUcHVMNlI0bUI1czhza1BVYk5CQ1U3SDZqSXZWYm10a0hueGFBQmRRdHZidDZICk5HdUdUaEkzOFdRVzFvWGMveWhJTk8rYlg0ajdpeXlhaEllRk1ZMVRMS0VkTHBBZ0cwS3Z2a1VUTzNVNmc1cysKRVQ2WGkzNm1hRHRXNzNTaGVsR3BBb0lCQVFDNTJDVk9nKysvS0RWeUdhSXpqeUdnSGMvWllFVms0WGZNbjRIcAo2aTdWSUV1NkhMdEt4T09TRXBjOUtmU2gwQUF6YWZNUVpiYXVQUUxhSG9xNHJLSWVYNDJYMXpDZTJpSFNrbUc2CjY4L0t2VUlEYkxGWmdpTHdid1A4eEZXVEpoT29hV1hpSExkVVV0dE4xZnorVjBWU28xYVN4SmVQQWZXQkszbFgKa3NhU3NBeHpHcUpSN3BuTWwxcTRrQy96ZUxlVWZYUW1KQUowZUlXL2l4QVcxa21xaDVXZHhtcHR2eVJaenllaQpWWWpjZlZoRHNlTFdDUlZhMDM4T3Z6LzM3TWZzN05meFpReHFNckRBT25TM3NXMDdqcWl3bDFTYktxQmk4Tm1iCndHOVlJYjFBUmE5WjRtazI1N3F2SElPZURrS1pRUUhQUmM0WlZuNGR5bzFUMVB4VEFvSUJBUUQ1Yk1HSnlyM2kKakJndVFKNEFTb0svNmNBODZGUk96SVdVWGt0S0FHakpqNHFCYWlTOHZ1UGJFSzhKMzM0a2VCQnplcmI0MmV0KwpyUVpMVnhacWV2clRpSEpWQWY0dFdmdXp2YSsxaGN6KzhrUnFscFRubTFPSVZ0Rm4wcHNYUGJDUDBjNHdEcEFtCnR4d3RlOWxIcVp2ZTNDRjVKZGRwL2RpdHljV3hudDBvM0cyQjYxRDlhOUQrWktmeFBiQjhiYzJFZHNlR1N6bG4Ka3RzbC8yWHZMakdGK1Ivd0F5dHNYeFJDcTdmc3E1ckV3RTQ0SXVhak5CY3lYdEt3TkVwN1dKaVF6SzJyOUtUbwpzV3BJa2w4SVp1aEhkb0N5cGp4ZmZCa0drS2FiOUVVZnNnc2tKVVV0V0t0cmVwM0JvK3RUMzgxVHIxUHVoc2FlCkNwVjgzYkJnTUlmL0FvSUJBUUM1dzlHK3d1a1I5Zm8rZkZzREdLNXU0MzNROW9kSENJejhiL0NuUmR2Y1UvVCsKS3h3WDUram1LVWtkN1dOSG42ZWp2T01tMzlVVUk0NUhRMEk4eU9zZ1d6a29yd1E5dFNNYjNoQnNqS3hmSm9EbQo5VzN0WDhUVyt0Rk1oMkJXdnpmbEhyMmxvRGlzeEVuTWF2c0JyWEJ3NHk1MWRLZGVYb0h0eE92ODJvMDlNVmF3CmM4NFBBZWFnZUNaSjJHN2wreVBkL096dDl3NnhKNHZoNC9xSUtWam5hYkhuVE44enBsMktuQ1F0QXp6VDBVTTYKSDRUemw0N09CQnk4Ty9XUU9CRzd4Ujh4ZjJWQ2ZqcnBXMFV5SE50b0xmaDIxRDNSQUhIamxJYWdSbUhHQWF1dgpwRGhjSzVJcVBNWUJwZ24ySXFEMW1lSmFNcGVLQWtmOE5XekJnR05iQW9JQkFEY0pYV2RIT2tFTUFxWHFFak1rClBkZVUvNXN3OUxGeWhhTW9iMXFEM3B0ak5mKzZhU0xReUR4bFlRby9xM2ptbVZLeDZEL2U3Z3pFcHhmbXZvWisKWG14Z3RrRzFyOTFYOXFkQm1zem1Ha2JETkgrRWZKVGlmRHNnVlJLZ3NTSWV0Wi8yZnBXeUVQTEtBc2l5aEU1QQp5YWtTVDd5SXNoQ3NONnlaVWlzUVQrS2RTUGlhOFFNS1VGSmg2WURjeU4yYjZQWnFzem9aelMvaWdzTE9xVEp1CnNmVEs5TllrRE9mYnp2K3JIbWwxc0Mvcjh4YUY5UGhjSFNZN01DZzdVWmdJQTVvbjM4YUtzL3k0Y1NoVVMrM3gKcngxZHM2WTM4aWhybktZTE13aEVqU0FpVEQwTWtFZ2lRYWowcEd5Kys4VkZJK3hzU292ZVoyTW1HTGcxdnA1cQo5d2NDZ2dFQkFMaG9QVVN2OHppckRZN0pybXJzZFZqbDA5eWhSZVNjYlJVQkZIanRqVkVXMEFRankrR1VBSzlvCnphaGNPUWJNdGU5cVpZSFYwTDJMVUZuYWdVM2dYblYyOCt4K05FVTc0cHoxT3ROYjBDTlpreWNWS04reXNJaTIKenlDZGZUVy8zbE82YUVDSVphMXM0U3VsWkp2V2VXdjlnemE5TkZjSmJyT3dMektVck12RGxjSW1xZ3JzNzJldQpzZjlzc0FJSmVscEdrVFR0OHd6YTM5Qm1PQVJVWXJOQ3hSYUc0TVc5RTZMSXRsTzFiVGpoOGhsd3k0YjZycWdDClN3czBleU02NjF1ak5RRkFyUkR0dlVJbklMY1B0dmM3VkxOWnFLMmRCbm5nRk9EZFBnbjZWeFltSU1wY0VuR3cKQTJnVTFzVnBOS3Rjc2ZMZVhISk1icERYci9UUy8rdz0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUF0Ukl2K1JkdWFRa2ZJVWRmcWh0dApQYVNNSC9NK0dCcjlCeG4rRUpqd2pxVm9XYThuODg4RHp4eWM2MU5RNXBMYWxqaXJPMDZVQTlKQllsY0g3RG1zCkFBemh1eGNndnJOTHhzODZzUDlieUs5Y1dOa2hlQUEwdFZSVUR1cEdlR29CZHBRMHovQ25lMUJ3UEJ2NUo4b2cKb0xYMUJJaVZiZkRyUEE0dm9Ud0FvRGRoRkxnVHpNSURBZG03NDB3QW9kSGdsNUUzVTduQUdyWDNicy9rV1R1Ugo3bUhUTzgzMVpNS0ZXdUtCYmR3UTAzLyt5NENyZVowSGhuLy9vU3d4RG5qY0psWFdtYXlBZ3FqLzhkN0psTlIvCnNNOHY4YzhMRStxRERteWFHNWVDMWVubHJBTEYzZkYva09jc3dNL0NZQmt1YmZxZUZWU0V4RHVVNW1sVnBLM3MKaGZDdk04em0zcW80MDE1VndIWnM3SDhkNjJjMWNacjV6T1hXOW9oM3J0S0xuZVg5VmY0a3pGb0FBN0dKY3dRYQorS3c3RG9LNXd5d0RDaDlVZWxqQi82SEFUb1FOVUVXUWMvMWY0bWZ4MHRlc1BXN1FmY1FTaVZXa3luVnpUS25NCk8vbzgrRzAyaWRXRWlwS2V1cVVUMmloM1FkMnRpRUxaK0tOVUJMKytsWm04dkdpOTUwSmd6ZFFMa2ZIa2tjNEsKR0U4Vzl1TkNXNGRseXdJV1RrZFMrZ0dkZ0RRSS9hV2l4amRINWNsWEpsU0xKMWpJamliYUZSR0FlaDhacllMZwpDVTM5T3FxU3RIcUlpWExNSkRsNnoyTE01c3hxdnRUTTJFc3JEdlJqZko5TlNHajNuN2lCei9VVW1GcFV3RGUrCkZONkk4MCtHQUpVQlZ1SUFta0hIRzYwQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=Als je nu opnieuw inlogt, zal je merken dat onze token een stuk langer is. Dit komt doordat de signature nu cryptografisch complexer is door het gebruik van de RS256-encryptie in de sleutels.
Hiermee is de token ook een stuk veiliger geworden, terwijl we hiervoor relatief weinig hebben hoeven aanpassen.
Conclusie
- We hebben een stateless authenticatiesysteem opgezet met JWT's, geschikt voor schaalbare applicaties zoals microservices.
- Een login-implementatie genereert tokens die andere API-endpoints kunnen gebruiken.
- Door een wrapper te gebruiken, hebben we boilerplate-code verminderd en foutafhandeling gestroomlijnd.
- Met asymmetrische sleutels (
RS256) hebben we de beveiliging verbeterd door validatie mogelijk te maken zonder de private sleutel bloot te geven.
Notitie
Als je dit echt verder wilt toepassen, zijn er een aantal zaken die je nog het beste onderzoekt:
- Blacklisting van tokens: Bijvoorbeeld wanneer een token onderschept wordt door een
hacker. - Refreshtokens: Om de access token (die wij gebruiken) een kortere levensduur te geven.
- Het gebruik van externe diensten: Zoals OAuth, Cognito, enzovoort.
- ...
In het kort: authenticatie is zeer complex en het correct implementeren ervan is moeilijk. Met de concepten die hier zijn besproken, heb je echter een basisidee van tokens en hoe deze kunnen worden gebruikt.