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