/* http-auth - Authentication Extensions to jdk.httpserver Copyright (C) 2021 Ulrich Hilger This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package de.uhilger.httpserver.auth; import com.sun.net.httpserver.Authenticator; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpPrincipal; import de.uhilger.httpserver.auth.session.AuthenticatedSession; import de.uhilger.httpserver.auth.session.Session; import de.uhilger.httpserver.auth.session.SessionManager; import de.uhilger.httpserver.auth.session.Sessions; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import java.security.Key; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import de.uhilger.httpserver.auth.realm.Realm; import de.uhilger.httpserver.auth.session.SweepThread; import io.jsonwebtoken.JwtException; /** * Ein Authenticator fuer HTTP-Anfragen, der den Mechanismus JSON Web Token * (JWT) verwendet. * * Dieser Authenticator fungiert als Ausgabeinstanz fuer JWT. Ueber die Methode * anmelden muessen Nutzer sich Authentisieren. Die Angaben der * Nutzer werden dabei gegen ein Nutzerverzeichnis auf Gueltigkeit ueberprueft. * So authentifizierte Nutzer erhalten zur Bestaetigung ihrer Authentifizierung * ein JWT ausgestellt. * * Der JWT wird als httpOnly Cookie namens * JWTAuthenticator.JWT_INDICATOR * an den Browser des Benutzers ausgegeben, der * weitere HTTP-Anfragen des Nutzers daraufhin mit dem JWT versieht. * * HTTP-Anfragen solcher Nutzer prueft dieser Authenticator auf gueltige JWT. * Anfragen mit gueltigem JWT werden dem Server als erfolgreich authentifiziert * gemeldet, Anfragen ohne gueltigen JWT werden dem Server als nicht * authentifiziert gemeldet. * * @author Ulrich Hilger * @version 1, 21. Mai 2021 */ public abstract class TokenAuthenticator extends Authenticator { /* Der Logger fuer diesen JWTAuthenticator */ private static final Logger logger = Logger.getLogger(TokenAuthenticator.class.getName()); public static final String STR_SLASH = "/"; public static final String STR_BLANK = " "; public static final String STR_AMP = "&"; public static final String STR_EQUAL = "="; public static final String STR_SEMICOLON = ";"; /** der Name des Cookie Headers */ public static final String COOKIE_HEADER = "Cookie"; public static final String SET_COOKIE_HEADER = "Set-Cookie"; /** Der Name des JSON-Web-Token-Cookies */ public static final String JWT_INDICATOR = "JWT"; public static final String LOGIN_JWT_INDICATOR = "LOGIN-JWT"; public static final String EXPIRES = "Expires"; public static final String PATH = "Path"; public static final String HTTP_ONLY = "HttpOnly"; public static final String SAME_SITE = "SameSite"; public static final String STRICT = "Strict"; /** * Ein Token ist 4 Stunden gueltig, also * 60 Sekunden x 60 Minuten x 4 Stunden = 14.400 Sekunden */ public static final long TOKEN_EXPIRATION = 14400; public static final int SC_UNAUTHORIZED = 401; public static final String HEADER_DATE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; /** * Das Nutzerverzeichnis, gegen das die bei der Anmeldung gemachten Angaben * gepueft werden sollen */ private Realm nutzerverzeichnis; /** der Schluessel zur Signatur von Tokens */ protected final Key key; /** die Sessions authentifizierter Nutzer */ private final SessionManager sessions; /** Der Kontext dieser App */ //protected final String ctx; protected Date lastSweep; /** * Ein Objekt der Klasse JWTAuthenticator erzeugen. */ public TokenAuthenticator() { //this.ctx = ctx; //nutzerverzeichnis = new TestRealm(); //paesse = new HashMap(); //sessions = new HashMap(); sessions = new Sessions(); key = Keys.secretKeyFor(SignatureAlgorithm.HS256); lastSweep = new Date(); } /** * Eine Anfrage authentifizieren. * * Hier wird geprueft, ob der Anfrage ein JSON Web Token beigefuegt ist. Wenn * kein JWT beigefuegt ist, wird Authenticator.Failure zurueckgegeben. Wenn * ein JWT beigefuegt ist, wird geprueft, ob der JWT gueltig ist. Wenn der JWT * nicht gueltig ist, wird Authenticator.Failure zurueckgegeben. Wenn der JWT * gueltig ist, wird Authenticator.Success zurueckgegeben. * * Ein JWT ist gueltig, wenn die Signatur des JWT mit dem Schluessel dieses * Authenticators uebereinstimmt, wenn die Nutzerkennung des JWT * uebereinstimmt mit der Nutzerkennung der Session fuer diesen JWT, wenn * die Gueltigkeitsdauer des tokens noch nicht abgelaufen ist und wenn * die Session noch nicht abgelaufen ist. * * Tokens bekommen eine feste Gueltigkeitsdauer. Es kann daher vorkommen, * dass eine Session laenger gueltig ist, als der Token. In diesem Fall * erscheint fuer diesen Benutzer eine erneute Aufforderung zur Anmeldung. * Mit erfolgreicher neuerlicher Anmeldung wird ein neuer Token ausgegeben * und in der Session gefuehrt. * * @param exchange das Objekt mit den HTTP Request und Reponse Informationen * @return Authenticator.Success, wenn de Authentifizierung erfolgreich ist, * Authenticator.Failure wenn nicht */ @Override public Result authenticate(HttpExchange exchange) { logger.info(exchange.getRequestURI().toString()); housekeeping(); String jwt = cookieLesen(exchange, JWT_INDICATOR); if (jwt != null) { //String jws = parts[1]; String userId = getUserId(jwt); if (userId != null) { try { Claims body = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt).getBody(); String jwtUserId = body.getSubject(); if(jwtUserId.equals(userId)) { Date now = new Date(); //Object sessionObj = sessions.get(jwt); Session s = sessions.get(jwt); if(s instanceof AuthenticatedSession) { AuthenticatedSession session = (AuthenticatedSession) s; if(session.getExp().after(now)) { // Session erneuern Date exp = Date.from(now.toInstant().plusSeconds(Session.EXPIRATION)); session = new AuthenticatedSession(); session.setId(jwt); session.setExp(exp); session.setUserId(userId); sessions.remove(jwt); //sessions.remove(jwt); sessions.add(session); //sessions.put(jwt, session); // erfolgreiche Authentifizierung melden try { HttpPrincipal pp = new HttpPrincipal(userId, nutzerverzeichnis.getName()); Result result = new Authenticator.Success(pp); return result; } catch (Exception ex) { Logger.getLogger(TokenAuthenticator.class.getName()).log(Level.SEVERE, null, ex); return new Authenticator.Failure(SC_UNAUTHORIZED); } } else { // Session abgelaufen sessions.remove(jwt); //sessions.remove(jwt); return login(exchange); } } else { // Session nicht gefunden return login(exchange); } } else { // JWT-Inhalt entspricht nicht der Benutzerkennung der Session return login(exchange); } } catch (JwtException ex) { // we *cannot* use the JWT as intended by its creator // z.B. Expiration Date ueberschritten sessions.remove(jwt); return login(exchange); } } else { // JWT ist nicht in der Liste der zur Zeit aktiven JWTs return login(exchange); } } else { // kein JWT vorhanden return login(exchange); } } public void setRealm(Realm realm) { this.nutzerverzeichnis = realm; } /** * Den Client zur Authentisierung auffordern * * @param exchange das Objekt mit Infos zu Anfrage und Antwort * @return das Ergebnis dieser Authenthisierung, * typischerweise Authenticator.Retry */ protected abstract Authenticator.Result login(HttpExchange exchange); protected void housekeeping() { Date sweepTime = Date.from(lastSweep.toInstant().plusSeconds(TOKEN_EXPIRATION)); Date now = new Date(); if(now.after(sweepTime)) { SweepThread sweeper = new SweepThread(sessions); sweeper.start(); } } /* public String cookieBilden(String name, String wert, Date exp, String path) { StringBuilder sb = new StringBuilder(); sb.append(name); sb.append(STR_EQUAL); sb.append(wert); sb.append(STR_SEMICOLON); sb.append(STR_BLANK); sb.append(EXPIRES); sb.append(STR_EQUAL); SimpleDateFormat fmt = new SimpleDateFormat(HEADER_DATE_PATTERN, Locale.US); sb.append(fmt.format(exp)); sb.append(STR_SEMICOLON); sb.append(PATH); sb.append(STR_EQUAL); sb.append(path); sb.append(STR_SEMICOLON); sb.append(STR_BLANK); sb.append(HTTP_ONLY); return sb.toString(); } */ public String cookieBilden(String name, String token, Date exp) { StringBuilder sb = new StringBuilder(); // JWT=jjhdksdghfds... sb.append(name); sb.append(STR_EQUAL); sb.append(token); // ; Expires=[Zeitpunkt] sb.append(STR_SEMICOLON); sb.append(STR_BLANK); sb.append(EXPIRES); sb.append(STR_EQUAL); SimpleDateFormat fmt = new SimpleDateFormat(HEADER_DATE_PATTERN, Locale.US); sb.append(fmt.format(exp)); // ; Path=/app-context sb.append(STR_SEMICOLON); sb.append(STR_BLANK); sb.append(PATH); sb.append(STR_EQUAL); //sb.append(path); sb.append(STR_SLASH); // ; SameSite=Strict sb.append(STR_SEMICOLON); sb.append(STR_BLANK); sb.append(SAME_SITE); sb.append(STR_EQUAL); sb.append(STRICT); // ; HttpOnly sb.append(STR_SEMICOLON); sb.append(STR_BLANK); sb.append(HTTP_ONLY); return sb.toString(); } /** * Wenn der JSON Web Token als Cookie an den Client gegeben wird, liefert die * Anfrage vom Client den JWT im Header als Cookie wie folgt: * *
   * Cookie: JWT=inhalt; anderer_cookie=anderer-inhalt
   * 
* * @param exchange das Objekt mit Angaben zu HTTP Request und Response * @return JSON Web Token aus dem Cookie Header oder null, wenn kein JWT * vorhanden ist */ public String cookieLesen(HttpExchange exchange, String key) { String wert = null; Headers headers = exchange.getRequestHeaders(); List cookies = headers.get(COOKIE_HEADER); if(cookies != null && cookies.size() > 0) { Iterator cookieIterator = cookies.iterator(); while (cookieIterator.hasNext() && wert == null) { Object cookieObj = cookieIterator.next(); if (cookieObj instanceof String) { String cookieStr = (String) cookieObj; String[] teile = cookieStr.split("; "); for(String cookie : teile) { String[] cookieTeil = cookie.split(STR_EQUAL); if(cookieTeil[0].equals(key)) { return cookieTeil[1]; } } } } } return wert; } /** * Die Nutzer-ID zu einem Token bestimmen. * * Pruefen, ob ein Token in der Liste der zur Zeit gueltigen Token * enthalten ist, also eine gueltige Session bezeichnet. Wenn ja, wird * zudem geprueft, ob es sich um eine AuthenticatedSession handelt. Wenn * ja, wird die Nutzer-ID dieser Session zurueckgegeben. * * Diese Methode prueft nicht, wie die Nutzer-ID des tokens lautet. Dies * wird in der Methode authenticate gemacht. * * @param token der Token, fuer den die Nutzer-ID bestimmt werden soll * @return Nutzer-ID der gueltigen Session oder null, wenn der Token keiner * gueltigen Session entspricht */ private String getUserId(String token) { Object sessionObj = sessions.get(token); if (sessionObj instanceof AuthenticatedSession) { AuthenticatedSession session = (AuthenticatedSession) sessionObj; return session.getUserId(); } else { return null; } } public String anmelden(String userId, String password) { if (nutzerverzeichnis.isValid(userId, password)) { return jwtErzeugen(userId); } else { return null; } } private String jwtErzeugen(String userId) { Date now = new Date(); Date exp = Date.from(now.toInstant().plusSeconds(TokenAuthenticator.TOKEN_EXPIRATION)); String jws = Jwts .builder() .setSubject(userId) .setIssuedAt(now) .setExpiration(exp) .signWith(key) .compact(); AuthenticatedSession session = new AuthenticatedSession(); session.setId(jws); exp = Date.from(now.toInstant().plusSeconds(Session.EXPIRATION)); session.setExp(exp); session.setUserId(userId); sessions.remove(jws); //sessions.remove(jws); sessions.add(session); //sessions.put(jws, session); return jws; } /** * Die Session mit einem bestimmten token ausloggen * * @param token der JWT, der ausgeloggt werden soll */ public void abmelden(String token) { //sessions.remove(token); sessions.remove(token); } }