Authentifizierung fuer Modul jdk.httpserver
ulrich
2021-06-04 83896588e20d7e532d0e2fdc17512772c29533a8
commit | author | age
9ee357 1 /*
c7d492 2   http-auth - Authentication Extensions to jdk.httpserver
9ee357 3   Copyright (C) 2021  Ulrich Hilger
U 4
5   This program is free software: you can redistribute it and/or modify
6   it under the terms of the GNU Affero General Public License as
7   published by the Free Software Foundation, either version 3 of the
8   License, or (at your option) any later version.
9
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU Affero General Public License for more details.
14
15   You should have received a copy of the GNU Affero General Public License
16   along with this program.  If not, see <https://www.gnu.org/licenses/>.
17  */
18 package de.uhilger.httpserver.auth;
19
20 import com.sun.net.httpserver.Authenticator;
21 import com.sun.net.httpserver.Headers;
22 import com.sun.net.httpserver.HttpExchange;
23 import com.sun.net.httpserver.HttpPrincipal;
24 import de.uhilger.httpserver.auth.session.AuthenticatedSession;
25 import de.uhilger.httpserver.auth.session.Session;
26 import de.uhilger.httpserver.auth.session.SessionManager;
27 import de.uhilger.httpserver.auth.session.Sessions;
28 import io.jsonwebtoken.Claims;
29 import io.jsonwebtoken.Jwts;
30 import io.jsonwebtoken.SignatureAlgorithm;
31 import io.jsonwebtoken.security.Keys;
32 import java.security.Key;
33 import java.text.SimpleDateFormat;
34 import java.util.Date;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.logging.Level;
39 import java.util.logging.Logger;
40 import de.uhilger.httpserver.auth.realm.Realm;
41 import de.uhilger.httpserver.auth.session.SweepThread;
42 import io.jsonwebtoken.JwtException;
43
44 /**
45  * Ein Authenticator fuer HTTP-Anfragen, der den Mechanismus JSON Web Token
46  * (JWT) verwendet.
47  *
48  * Dieser Authenticator fungiert als Ausgabeinstanz fuer JWT. Ueber die Methode
49  * <code>anmelden</code> muessen Nutzer sich Authentisieren. Die Angaben der
50  * Nutzer werden dabei gegen ein Nutzerverzeichnis auf Gueltigkeit ueberprueft.
51  * So authentifizierte Nutzer erhalten zur Bestaetigung ihrer Authentifizierung
52  * ein JWT ausgestellt.
53  *
54  * Der JWT wird als httpOnly Cookie namens 
55  * <code>JWTAuthenticator.JWT_INDICATOR</code>  
56  * an den Browser des Benutzers ausgegeben, der
57  * weitere HTTP-Anfragen des Nutzers daraufhin mit dem JWT versieht.
58  *
59  * HTTP-Anfragen solcher Nutzer prueft dieser Authenticator auf gueltige JWT.
60  * Anfragen mit gueltigem JWT werden dem Server als erfolgreich authentifiziert
61  * gemeldet, Anfragen ohne gueltigen JWT werden dem Server als nicht
62  * authentifiziert gemeldet.
63  *
64  * @author Ulrich Hilger
65  * @version 1, 21. Mai 2021
66  */
67 public abstract class TokenAuthenticator extends Authenticator {
68   
69   /* Der Logger fuer diesen JWTAuthenticator */
70   private static final Logger logger = Logger.getLogger(TokenAuthenticator.class.getName());
71   
72   public static final String STR_SLASH = "/";
73   public static final String STR_BLANK = " ";
74   public static final String STR_AMP = "&";
75   public static final String STR_EQUAL = "=";
76   public static final String STR_SEMICOLON = ";";  
77
78   /** der Name des Cookie Headers */
79   public static final String COOKIE_HEADER = "Cookie";
80
81   public static final String SET_COOKIE_HEADER = "Set-Cookie";
82
83   /** Der Name des JSON-Web-Token-Cookies */
84   public static final String JWT_INDICATOR = "JWT";
85   
86   public static final String LOGIN_JWT_INDICATOR = "LOGIN-JWT";
87   
88   public static final String EXPIRES = "Expires";
89   public static final String PATH = "Path";
90   public static final String HTTP_ONLY = "HttpOnly";
91   public static final String SAME_SITE = "SameSite";
92   public static final String STRICT = "Strict";
93   
94   /** 
95    * Ein Token ist 4 Stunden gueltig, also 
96    * 60 Sekunden x 60 Minuten x 4 Stunden = 14.400 Sekunden 
97    */
98   public static final long TOKEN_EXPIRATION = 14400;
99   
100   public static final int SC_UNAUTHORIZED = 401;
101
102   public static final String HEADER_DATE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z";
103   
104   /**
105    * Das Nutzerverzeichnis, gegen das die bei der Anmeldung gemachten Angaben
106    * gepueft werden sollen
107    */
696a4b 108   private Realm nutzerverzeichnis;
9ee357 109
U 110   /** der Schluessel zur Signatur von Tokens */
111   protected final Key key;
112
113   /** die Sessions authentifizierter Nutzer */ 
114   private final SessionManager sessions;
115   
116   /** Der Kontext dieser App */
117   //protected final String ctx;
118   
119   protected Date lastSweep;
120   
121   /**
122    * Ein Objekt der Klasse <code>JWTAuthenticator</code> erzeugen.
123    */
124   public TokenAuthenticator() {
125     //this.ctx = ctx;
696a4b 126     //nutzerverzeichnis = new TestRealm();
9ee357 127     //paesse = new HashMap();
U 128     //sessions = new HashMap();
129     sessions = new Sessions();
130     key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
131     lastSweep = new Date();
132   }
133
134   /**
135    * Eine Anfrage authentifizieren.
136    *
137    * Hier wird geprueft, ob der Anfrage ein JSON Web Token beigefuegt ist. Wenn
138    * kein JWT beigefuegt ist, wird Authenticator.Failure zurueckgegeben. Wenn
139    * ein JWT beigefuegt ist, wird geprueft, ob der JWT gueltig ist. Wenn der JWT
140    * nicht gueltig ist, wird Authenticator.Failure zurueckgegeben. Wenn der JWT
141    * gueltig ist, wird Authenticator.Success zurueckgegeben.
142    *
143    * Ein JWT ist gueltig, wenn die Signatur des JWT mit dem Schluessel dieses
144    * Authenticators uebereinstimmt, wenn die Nutzerkennung des JWT 
145    * uebereinstimmt mit der Nutzerkennung der Session fuer diesen JWT, wenn 
146    * die Gueltigkeitsdauer des tokens noch nicht abgelaufen ist und wenn 
147    * die Session noch nicht abgelaufen ist.
148    * 
149    * Tokens bekommen eine feste Gueltigkeitsdauer. Es kann daher vorkommen, 
150    * dass eine Session laenger gueltig ist, als der Token. In diesem Fall 
151    * erscheint fuer diesen Benutzer eine erneute Aufforderung zur Anmeldung. 
152    * Mit erfolgreicher neuerlicher Anmeldung wird ein neuer Token ausgegeben 
153    * und in der Session gefuehrt.
154    *
155    * @param exchange das Objekt mit den HTTP Request und Reponse Informationen
156    * @return Authenticator.Success, wenn de Authentifizierung erfolgreich ist,
157    * Authenticator.Failure wenn nicht
158    */
159   @Override
160   public Result authenticate(HttpExchange exchange) {
161     logger.info(exchange.getRequestURI().toString());
162     housekeeping();
163     String jwt = cookieLesen(exchange, JWT_INDICATOR);
164     if (jwt != null) {
165       //String jws = parts[1];
166       String userId = getUserId(jwt);
167       if (userId != null) {
168         try {
169           Claims body = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt).getBody();
170           String jwtUserId = body.getSubject();
171           if(jwtUserId.equals(userId)) {
172             Date now = new Date();
173             //Object sessionObj = sessions.get(jwt);
174             Session s = sessions.get(jwt);
175             if(s instanceof AuthenticatedSession) {
176               AuthenticatedSession session = (AuthenticatedSession) s;
177               if(session.getExp().after(now)) {
178                 // Session erneuern
179                 Date exp = Date.from(now.toInstant().plusSeconds(Session.EXPIRATION));       
180                 session = new AuthenticatedSession();
181                 session.setId(jwt);
182                 session.setExp(exp);
183                 session.setUserId(userId);
184                 sessions.remove(jwt);
185                 //sessions.remove(jwt);
186                 sessions.add(session);
187                 //sessions.put(jwt, session);
188                 // erfolgreiche Authentifizierung melden
189                 try {
190                   HttpPrincipal pp = new HttpPrincipal(userId, nutzerverzeichnis.getName());
191                   Result result = new Authenticator.Success(pp);
192                   return result;
193                 } catch (Exception ex) {
194                   Logger.getLogger(TokenAuthenticator.class.getName()).log(Level.SEVERE, null, ex);
195                   return new Authenticator.Failure(SC_UNAUTHORIZED);
196                 }
197               } else {
198                 // Session abgelaufen
199                 sessions.remove(jwt);
200                 //sessions.remove(jwt);
201                 return login(exchange);          
202               }
203             } else {
204               // Session nicht gefunden
205               return login(exchange);          
206             }
207           } else {
208             // JWT-Inhalt entspricht nicht der Benutzerkennung der Session
209             return login(exchange);
210           }
211         }
212         catch (JwtException ex) {
213           // we *cannot* use the JWT as intended by its creator
214           // z.B. Expiration Date ueberschritten
215           sessions.remove(jwt);
216           return login(exchange);
217         }        
218       } else {
219         // JWT ist nicht in der Liste der zur Zeit aktiven JWTs
220         return login(exchange);
221       }
222     } else {
223       // kein JWT vorhanden
224       return login(exchange);
225     }
226   }
227   
696a4b 228   public void setRealm(Realm realm) {
U 229     this.nutzerverzeichnis = realm;
230   }
231   
9ee357 232   /**
U 233    * Den Client zur Authentisierung auffordern
234    * 
235    * @param exchange das Objekt mit Infos zu Anfrage und Antwort
236    * @return das Ergebnis dieser Authenthisierung, 
237    * typischerweise Authenticator.Retry
238    */
239   protected abstract Authenticator.Result login(HttpExchange exchange);
240   
241   protected void housekeeping() {
242     Date sweepTime = Date.from(lastSweep.toInstant().plusSeconds(TOKEN_EXPIRATION));
243     Date now = new Date();
244     if(now.after(sweepTime)) {
245       SweepThread sweeper = new SweepThread(sessions);
246       sweeper.start();
247     }
248   }
249   
250   /*
251   public String cookieBilden(String name, String wert, Date exp, String path) {
252     StringBuilder sb = new StringBuilder();
253     sb.append(name);
254     sb.append(STR_EQUAL);
255     sb.append(wert);
256     sb.append(STR_SEMICOLON);
257     sb.append(STR_BLANK);
258     sb.append(EXPIRES);
259     sb.append(STR_EQUAL);
260     SimpleDateFormat fmt = new SimpleDateFormat(HEADER_DATE_PATTERN, Locale.US);
261     sb.append(fmt.format(exp));
262     sb.append(STR_SEMICOLON);
263     sb.append(PATH);
264     sb.append(STR_EQUAL);
265     sb.append(path);
266     sb.append(STR_SEMICOLON);
267     sb.append(STR_BLANK);
268     sb.append(HTTP_ONLY);
269     return sb.toString();
270   }
271   */
272   
273   
274   public String cookieBilden(String name, String token, Date exp) {
275     StringBuilder sb = new StringBuilder();
276     // JWT=jjhdksdghfds...
277     sb.append(name);
278     sb.append(STR_EQUAL);
279     sb.append(token);
280     // ; Expires=[Zeitpunkt]
281     sb.append(STR_SEMICOLON);
282     sb.append(STR_BLANK);
283     sb.append(EXPIRES);
284     sb.append(STR_EQUAL);
285     SimpleDateFormat fmt = new SimpleDateFormat(HEADER_DATE_PATTERN, Locale.US);
286     sb.append(fmt.format(exp));
287     // ; Path=/app-context
288     sb.append(STR_SEMICOLON);
289     sb.append(STR_BLANK);
290     sb.append(PATH);
291     sb.append(STR_EQUAL);
292     //sb.append(path);
293     sb.append(STR_SLASH);
294     // ; SameSite=Strict
295     sb.append(STR_SEMICOLON);
296     sb.append(STR_BLANK);
297     sb.append(SAME_SITE);
298     sb.append(STR_EQUAL);
299     sb.append(STRICT);
300     // ; HttpOnly
301     sb.append(STR_SEMICOLON);
302     sb.append(STR_BLANK);
303     sb.append(HTTP_ONLY);
304     return sb.toString();
305   }
306   
307   
308   /**
309    * Wenn der JSON Web Token als Cookie an den Client gegeben wird, liefert die
310    * Anfrage vom Client den JWT im Header als Cookie wie folgt:
311    *
312    * <pre>
313    * Cookie: JWT=inhalt; anderer_cookie=anderer-inhalt
314    * </pre>
315    *
316    * @param exchange das Objekt mit Angaben zu HTTP Request und Response
317    * @return JSON Web Token aus dem Cookie Header oder null, wenn kein JWT
318    * vorhanden ist
319    */
320   public String cookieLesen(HttpExchange exchange, String key) {
321     String wert = null;
322     Headers headers = exchange.getRequestHeaders();
323     List<String> cookies = headers.get(COOKIE_HEADER);
324     if(cookies != null && cookies.size() > 0) {
325       Iterator cookieIterator = cookies.iterator();
326       while (cookieIterator.hasNext() && wert == null) {
327         Object cookieObj = cookieIterator.next();
328         if (cookieObj instanceof String) {
329           String cookieStr = (String) cookieObj;
330           String[] teile = cookieStr.split("; ");
331           for(String cookie : teile) {
332             String[] cookieTeil = cookie.split(STR_EQUAL);
333             if(cookieTeil[0].equals(key)) {
334               return cookieTeil[1];
335             }
336           }
337         }
338       }
339     }
340     return wert;
341   }
342
343   /**
344    * Die Nutzer-ID zu einem Token bestimmen.
345    * 
346    * Pruefen, ob ein Token in der Liste der zur Zeit gueltigen Token 
347    * enthalten ist, also eine gueltige Session bezeichnet. Wenn ja, wird 
348    * zudem geprueft, ob es sich um eine AuthenticatedSession handelt. Wenn 
349    * ja, wird die Nutzer-ID dieser Session zurueckgegeben.
350    * 
351    * Diese Methode prueft nicht, wie die Nutzer-ID des tokens lautet. Dies 
352    * wird in der Methode <code>authenticate</code> gemacht.
353    * 
354    * @param token  der Token, fuer den die Nutzer-ID bestimmt werden soll
355    * @return Nutzer-ID der gueltigen Session oder null, wenn der Token keiner 
356    * gueltigen Session entspricht
357    */
358   private String getUserId(String token) {
359     Object sessionObj = sessions.get(token);
360     if (sessionObj instanceof AuthenticatedSession) {
361       AuthenticatedSession session = (AuthenticatedSession) sessionObj;
362       return session.getUserId();
363     } else {
364       return null;
365     }
366   }
367
368   public String anmelden(String userId, String password) {
369     if (nutzerverzeichnis.isValid(userId, password)) {
370       return jwtErzeugen(userId);
371     } else {
372       return null;
373     }
374   }
375
376   private String jwtErzeugen(String userId) {
377     Date now = new Date();
378     Date exp = Date.from(now.toInstant().plusSeconds(TokenAuthenticator.TOKEN_EXPIRATION));       
379     String jws = Jwts
380             .builder()
381             .setSubject(userId)
382             .setIssuedAt(now)
383             .setExpiration(exp)
384             .signWith(key)
385             .compact();
386     AuthenticatedSession session = new AuthenticatedSession();
387     session.setId(jws);
388     exp = Date.from(now.toInstant().plusSeconds(Session.EXPIRATION));       
389     session.setExp(exp);
390     session.setUserId(userId);
391     sessions.remove(jws);
392     //sessions.remove(jws);
393     sessions.add(session);
394     //sessions.put(jws, session);
395     return jws;
396   }
397   
398   /**
399    * Die Session mit einem bestimmten token ausloggen
400    *
401    * @param token der JWT, der ausgeloggt werden soll
402    */
403   public void abmelden(String token) {
404     //sessions.remove(token);
405     sessions.remove(token);
406   }
407 }