/*
jwtTest - JSON Web Token Testimplementierung
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 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
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);
}
}