DEV Community

nikosst
nikosst

Posted on • Edited on

Πώς δουλεύει το JWT σε ένα Client Flow

JWT Jurnay


Εισαγωγή — Τι είναι το JWT (JSON Web Token)

Το JWT (JSON Web Token) είναι ένα compact, URL-safe φορμάτ για να μεταφέρεις αξιώσεις (claims) ανάμεσα σε κόμβους (client ⇄ server). Δηλαδή μετά και την αυθεντικοποίηση απο τον server ακολουθεί τον χρήστη στο ταξίδι του μέσα στην εφαρμογή για όσο χρονικό διάστημα έχει οριστεί να διαρκεί η συνεδρία.

Ένα τυπικό JWT αποτελείται από τρία μέρη, χωρισμένα με τελείες και έχει την παρακάτω μορφή.

xxxxx.yyyyy.zzzzz

όπου

  • xxxxx → Header (ο τύπος + αλγόριθμος υπογραφής)
  • yyyyy → Payload (τα claims)
  • zzzzz → Signature (η ψηφιακή υπογραφή)

παμε να τα δούμε αναλυτικά.

1. Header περιγράφει τον αλγόριθμο υπογραφής και το τύπο, π.χ. { "alg": "HS256", "typ": "JWT" }.

2. Payload — JSON με claims (π.χ. sub, exp, iat, roles, custom claims).

Το Payload μέσα στο JWT, είναι ουσιαστικά η καρδιά του token, εκεί που αποθηκεύονται οι πληροφορίες (claims) για τον χρήστη ή τη συνεδρία.

Το payload είναι ένα JSON αντικείμενο με key–value ζεύγη, που περιέχουν πληροφορίες για το:

  • ποιος είναι ο χρήστης,
  • ποιος εξέδωσε το token,
  • πότε δημιουργήθηκε,
  • πότε λήγει,
  • και άλλα custom claims.

Παράδειγμα Payload

{ "sub": "1234567890", "name": "John Doe", "role": "admin", "iat": 1713280056, "exp": 1713283656, "iss": "myapi.com", "aud": "myfrontend.com" } 
Enter fullscreen mode Exit fullscreen mode

Αυτό το JSON, όταν το μετατρέψουμε σε Base64URL, γίνεται το μεσαίο κομμάτι του JWT.

Claims example

Claim Τι κάνει Πώς χρησιμοποιείται
sub “subject” — μοναδικό id χρήστη Συνήθως το user ID από τη βάση
exp “expiration” — λήξη σε UNIX time Server απορρίπτει αν τώρα > exp
iat “issued at” — δημιουργία σε UNIX time Για logging/validation
iss “issuer” — ποιος υπέγραψε Ελέγχεται στην επαλήθευση
aud “audience” — ποιος πρέπει να το δεχτεί Το API ελέγχει ότι ταιριάζει
roles Custom — λίστα ρόλων Για authorization logic
permissions Custom — λίστα δικαιωμάτων Για fine-grained access control

3. Signature HMAC ή RSA/ECDSA signature πάνω στο Base64Url
(header) + "." + Base64Url(payload).**

Τι υπογράφεται

Όταν δημιουργούμε ένα JWT, δεν υπογράφουμε όλο το token·
υπογράφουμε μόνο αυτά τα δύο μέρη ενωμένα:

Base64Url(header) + "." + Base64Url(payload)

Δηλαδή πρώτα κωδικοποιούνται το header και το payload σε **Base64Url**,
μετά ενώνονται με μια τελεία . και πάνω σε αυτό το κείμενο δημιουργείται η υπογραφή (signature).

Τι είναι η υπογραφή (Signature)

Η υπογραφή είναι ένας κρυπτογραφικός έλεγχος ακεραιότητας.

Εξασφαλίζει ότι κανείς δεν μπορεί να αλλάξει το payload ή το header χωρίς να χαλάσει η υπογραφή.

Δηλαδή:

  • Αν κάποιος τροποποιήσει το payload (π.χ. role = "admin"), ο server το καταλαβαίνει αμέσως, γιατί η υπογραφή δεν θα ταιριάζει.

Είδη υπογραφών

Υπάρχουν δύο κύριοι τύποι αλγορίθμων υπογραφής στα JWTs:

HMAC (συμμετρική υπογραφή)

  • Παράδειγμα: HS256, HS384, HS512

  • Χρησιμοποιεί το ίδιο “secret key” και για την υπογραφή και για την επαλήθευση.

  • Απλό και γρήγορο, ιδανικό για single-service APIs.

signature = HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)

Ο server και ο issuer πρέπει να έχουν το ίδιο secret.

RSA / ECDSA (ασύμμετρη υπογραφή)

Παράδειγμα: RS256, ES256, ES512

Χρησιμοποιεί ζεύγος κλειδιών:

 **Private key** → για υπογραφή (στο auth server) **Public key** → για επαλήθευση (σε άλλα services) 
Enter fullscreen mode Exit fullscreen mode
  • Ιδανικό για distributed ή microservice περιβάλλοντα.

Δηλαδή:

signature = RSA_sign(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)

Και μετά η επαλήθευση:

RSA_verify(signature, publicKey)

Πώς μοιάζει στο τελικό JWT

Ένα υπογεγραμμένο JWT έχει τη μορφή:

<Header>.<Payload>.<Signature>

Παράδειγμα:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiSm9obiBEb2UifQ.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ


Το token είναι αυτοπεριγραφικό — ο server μπορεί να ελέγξει την εγκυρότητα χωρίς να κάνει lookup σε βάση (εκτός αν χρησιμοποιήσεις λίστα απορριφθέντων tokens ή blacklisting). Είναι κατάλληλο για stateless authentication.


Πότε το χρησιμοποιούμε

  • Authentication / Authorization σε APIs (REST, GraphQL).

  • Single Page Applications (SPA) όπου ο client χρησιμοποιεί token για κλήσεις προς backend.

  • Microservices — μεταφορά claims μεταξύ υπηρεσιών.
    Όχι κατάλληλο για πολύ ευαίσθητα δεδομένα χωρίς επιπλέον μέτρα (η payload είναι base64, όχι κρυπτογραφημένη).


Γενική ροή (χάρτης των 8 βημάτων)

Θα προσπαθήσουμε να κάνουμε μία ανατομία και περιγραφή του ταξιδιού των βημάτων που ξεκινα και ακολουθεί τον χρήστη καθ'όλη την διάρκειά του μέσα σε μια συνεδρία.

Βήμα 1 — Ο client παρέχει credentials

Ο χρήστης/πελάτης στέλνει credentials (username/password, OAuth code, API key) στον authentication endpoint (/auth/login). Σημεία προσοχής:

  • Χρησιμοποιούμε HTTPS πάντα.

  • Περιορίζουμε rate (rate limiting) για login endpoints.

  • Δεν στέλνουμε credentials σε URLs (avoid GET), αλλά σε body POST

Βήμα 2 — Ο server επαληθεύει τα credentials

Ο server ελέγχει τα credentials:

  • Password hashing: bcrypt/argon2 (όχι MD5/SHA1).

  • Έλεγχος account lockouts, MFA/2FA αν απαιτείται.

  • Αν έγκυρα → συνεχίζουμε στην επιλογή claims (τι θα μπει στο JWT).

Βήμα 3 — Ο server δημιουργεί & υπογράφει το JWT

Ο server δημιουργεί το payload με απαραίτητα claims:

  • iss (issuer), sub (subject/user id), aud (audience), exp (expiration), iat (issued at), nbf (not before), custom (roles, permissions).

Υπογράφει με:

  • Symmetric HMAC (HS256) — απλό, κοινό μυστικό (secret key).

  • Asymmetric RSA/ECDSA (RS256/ES256) — καλύτερο για distributed systems: server υπογράφει με private key, consumers επαληθεύουν με public key.

Σημείο αποφάσεων:

  • Access token: μικρός χρόνος ζωής (π.χ. 5–15 min).

  • Refresh token: μεγαλύτερος χρόνος ζωής, χειρισμός πιο προσεκτικός (βλέπε βήμα 8).

Βήμα 4 — Ο server επιστρέφει το signed JWT στον client

Συνήθως response:

{ "access_token": "eyJhbGciOi...", "expires_in": 900, "refresh_token": "long-random-string-if-used" } 
Enter fullscreen mode Exit fullscreen mode

Προσοχή:

  • Το JWT δεν πρέπει να εκτίθεται σε μη ασφαλή περιβάλλοντα.

  • Για browsers, προτίμησε httpOnly secure cookies για access tokens αν μπορείς, για μείωση XSS risk. Αν χρησιμοποιήσεις localStorage/sessionStorage, λάβε μέτρα κατά XSS.

Βήμα 5 — Αποθήκευση του JWT (client side)

Επιλογές:

httpOnly cookie (προτεινόμενο για browsers): μειώνει XSS αλλά χρειάζεται CSRF προστασία.

localStorage: ευκολότερο αλλά ευάλωτο σε XSS.

in-memory (μέσω state) + refresh token σε cookie — μειώνει επιθέσεις αλλά χάνει persistence σε reload.

Σε server side: μπορείς να καταγράψεις refresh tokens (ταυτοποιήσιμα) για blacklisting/rotation.

Βήμα 6 — Ο client συμπεριλαμβάνει JWT σε κάθε API request

Τυπικά στο header:

Authorization: Bearer <access_token> 
Enter fullscreen mode Exit fullscreen mode

Ο server, σε κάθε protected endpoint, διαβάζει header, παίρνει token, και προχωρά σε validation.

Βήμα 7 — Ο server επαληθεύει το JWT (multiple checks)

  • Signature validation: HMAC με shared secret ή RSA verify με public key.

  • exp/nbf/iat: έλεγχος για expiration / future tokens.

  • iss/aud: επιβεβαιώνεις issuer και audience.

  • revocation/blacklist: αν χρησιμοποιείς blacklist ή token id (jti), ελέγχεις αν το token απορρίφθηκε.

  • scopes/roles: έλεγχος εξουσιοδότησης για το resource.

  • alg check: απαγόρευσε "alg:none" και αποδέξου μόνο τα αλγόριθμα που επιτρέπεις.

Βήμα 8 — Refresh token για νέο JWT όταν λήξει το τρέχον

  • Client χρησιμοποιεί refresh token (συνήθως αποθηκευμένο ασφαλώς) για να ζητήσει νέο access token.

  • Refresh endpoint ελέγχει refresh token (έγκυρο, δεν έχει ανακληθεί) και εκδίδει νέο access token — και πιθανώς νέο refresh token (rotation).

  • Rotation: κάθε χρήση του refresh token εκδίδει νέο refresh token και ο παλιός γίνεται άκυρος — μειώνει replay attacks.

  • Revoke on logout: αποθηκεύεις refresh tokens/identifiers και τα μαρκάρεις ως revoked όταν ο χρήστης κάνει logout.


Καλές πρακτικές ασφάλειας

  • Χρησιμοποίησε HTTPS πάντα.

  • Προτίμησε RS256 (asymmetric) αν έχεις πολλαπλούς verifiers.

  • Κράτησε μικρό exp για access tokens.

  • Χρησιμοποίησε refresh tokens ασφαλώς (server side storage / httpOnly cookie).

  • Ενεργοποίησε CSP και απαλλαγή από XSS για web clients.

  • Εφάρμοσε token revocation και monitoring.

  • Μην αποθηκεύεις ευαίσθητα PII στο payload.


Παράδειγμα σε C# (ASP.NET Core) — Δημιουργία & Επαλήθευση JWT

Παρακάτω πρακτικό παράδειγμα που δείχνει:

  1. Δημιουργία access token (HS256 για απλότητα).

  2. Επαλήθευση token (middleware).

  3. Refresh token απλό παράδειγμα (αποθήκευση σε DB).

*Σημείωση: *

Για παραγωγή, προτίμησε RS256 (private/public keys) και secure storage των κλειδιών (Key Vault).

1) appsettings.json (μικρό απόσπασμα)

{ "Jwt": { "Key": "very_long_random_secret_key_here_change_in_prod", "Issuer": "my.api", "Audience": "my.api.clients", "AccessTokenExpirationMinutes": 15, "RefreshTokenExpirationDays": 14 } } 
Enter fullscreen mode Exit fullscreen mode

2) Models: TokenResponse και RefreshToken entity

public class TokenResponse { public string AccessToken { get; set; } public int ExpiresIn { get; set; } // seconds public string RefreshToken { get; set; } } public class RefreshToken { public int Id { get; set; } public string Token { get; set; } // long random string public string UserId { get; set; } public DateTime Expires { get; set; } public bool IsRevoked { get; set; } public DateTime Created { get; set; } } 
Enter fullscreen mode Exit fullscreen mode

3) Utility: Generate JWT & Refresh token

using System.IdentityModel.Tokens.Jwt; using Microsoft.IdentityModel.Tokens; using System.Security.Claims; using System.Text; public class TokenService { private readonly IConfiguration _config; public TokenService(IConfiguration config) { _config = config; } public TokenResponse GenerateTokens(string userId, IList<string> roles) { var jwtSettings = _config.GetSection("Jwt"); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var now = DateTime.UtcNow; var expires = now.AddMinutes(double.Parse(jwtSettings["AccessTokenExpirationMinutes"])); var claims = new List<Claim> { new Claim(JwtRegisteredClaimNames.Sub, userId), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) }; claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r))); var token = new JwtSecurityToken( issuer: jwtSettings["Issuer"], audience: jwtSettings["Audience"], claims: claims, notBefore: now, expires: expires, signingCredentials: creds ); var accessToken = new JwtSecurityTokenHandler().WriteToken(token); // Create refresh token (secure random string) var refreshToken = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); return new TokenResponse { AccessToken = accessToken, ExpiresIn = (int)(expires - now).TotalSeconds, RefreshToken = refreshToken }; } } 
Enter fullscreen mode Exit fullscreen mode

4) Configure Authentication in Startup / Program

// In Program.cs (ASP.NET Core 6+) var builder = WebApplication.CreateBuilder(args); var jwtSettings = builder.Configuration.GetSection("Jwt"); var key = Encoding.UTF8.GetBytes(jwtSettings["Key"]); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtSettings["Issuer"], ValidateAudience = true, ValidAudience = jwtSettings["Audience"], ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30) // μικρή ανοχή για clock drift }; // Αν θέλεις να δέχεσαι access tokens από cookie, etc, ρυθμίσεις εδώ. }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); 
Enter fullscreen mode Exit fullscreen mode

5) Login endpoint (παράδειγμα)

[HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginDto dto) { // 1. Validate credentials (compare hashed password) var user = await _userRepo.FindByUsernameAsync(dto.Username); if (user == null || !VerifyPassword(dto.Password, user.PasswordHash)) return Unauthorized(); // 2. Generate tokens var roles = await _userRepo.GetRolesAsync(user.Id); var tokens = _tokenService.GenerateTokens(user.Id, roles); // 3. Persist refresh token in DB with association to user var refreshEntity = new RefreshToken { Token = tokens.RefreshToken, UserId = user.Id, Expires = DateTime.UtcNow.AddDays(int.Parse(_config["Jwt:RefreshTokenExpirationDays"])), Created = DateTime.UtcNow }; _db.RefreshTokens.Add(refreshEntity); await _db.SaveChangesAsync(); // 4. Return tokens; for web-app, consider setting refresh token as httpOnly cookie: return Ok(tokens); } 
Enter fullscreen mode Exit fullscreen mode

6) Refresh endpoint (rotation & revoke example)

[HttpPost("refresh")] public async Task<IActionResult> Refresh([FromBody] RefreshRequest request) { var stored = await _db.RefreshTokens.SingleOrDefaultAsync(t => t.Token == request.RefreshToken); if (stored == null || stored.IsRevoked || stored.Expires < DateTime.UtcNow) return Unauthorized(); // Optional: Revoke existing refresh token (rotation) stored.IsRevoked = true; // Issue new tokens var roles = await _userRepo.GetRolesAsync(stored.UserId); var newTokens = _tokenService.GenerateTokens(stored.UserId, roles); // Save new refresh token record var newRefresh = new RefreshToken { Token = newTokens.RefreshToken, UserId = stored.UserId, Expires = DateTime.UtcNow.AddDays(int.Parse(_config["Jwt:RefreshTokenExpirationDays"])), Created = DateTime.UtcNow }; _db.RefreshTokens.Add(newRefresh); await _db.SaveChangesAsync(); return Ok(newTokens); } 
Enter fullscreen mode Exit fullscreen mode

Πώς γίνεται ο έλεγχος στο endpoint (Server-side validation)

Με το middleware AddJwtBearer παραπάνω, πολλά checks γίνονται αυτόματα: signature, exp, issuer, audience. Επιπλέον:

  • Αν χρησιμοποιείς jti (token id), μπορείς να κάνεις lookup σε blacklist/deny-list στο DB για επιπλέον ασφάλεια.

  • Να ελέγχεις claims για scopes/roles στο authorization layer.


Πρακτικά σενάρια & προβλήματα που συναντώνται

  • Clock skew: Σε διακομιστές με διαφορετικό χρόνο, χρησιμοποίησε ClockSkew στα validation params.

  • Token revocation: Stateless JWT δεν επιτρέπει εύκολο revoke — λύσεις: store jti σε blacklist ή μικρό exp + refresh tokens.

  • Large payloads: Μην βάζεις πολλά attributes στο token — προτίμησε reference tokens (server side lookup).

  • Logout across devices: πρέπει να σκέφτεσαι revoke των refresh tokens που ανήκουν στο user/device.


Advanced: RS256 & JWKS (Public Key Distribution)

Για microservices ή third-party verification:

  • Υπογράφεις με RS256 (private key).

  • Εκθέτεις public keys μέσω JWKS endpoint (/.well-known/jwks.json) — clients θα τραβάνε τα keys και θα ελέγχουν signature.

  • Σε ASP.NET Core, AddJwtBearer μπορεί να ρυθμιστεί να παίρνει Authority (OpenID Connect) και να κατεβάζει keys αυτόματα.


Συνοψίζοντας (takeaways)

  • JWT είναι πολύ χρήσιμο για stateless auth αλλά πρέπει να το σκεφτείς σαν μέρος συστήματος: short lived access tokens + secure refresh tokens + proper validation = καλός συνδυασμός.

  • Ασφάλεια: HTTPS, secure storage, key management, token rotation/revoke.

  • Σε production, προτίμησε asymmetric signing και χρήση μυστικών/κλειδιών από secure vault.

Top comments (0)