OAuth-Unterstuetzung fuer jdk.httpserver
ulrich
2021-07-05 8c1928946cb3b4f2d9ead70c7362ce1dbe045fa4
commit | author | age
7ecde3 1 /*
U 2   http-oauth - OAuth Extensions to jdk.httpserver
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.oauth;
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.realm.Realm;
4bf4d1 25 import de.uhilger.httpserver.base.HttpResponder;
7ecde3 26 import io.jsonwebtoken.Claims;
U 27 import io.jsonwebtoken.JwtException;
28 import io.jsonwebtoken.Jwts;
29 import io.jsonwebtoken.SignatureAlgorithm;
30 import io.jsonwebtoken.security.Keys;
31 import java.io.IOException;
32 import java.security.Key;
33 import java.util.Date;
34 import java.util.logging.Level;
35 import java.util.logging.Logger;
36
37 /**
38  * Die Klasse Authenticator authentifziert gem&auml;&szlig; OAuth-Spezifikation 
39  * "The OAuth 2.0 Authorization Framework: Bearer Token Usage"
40  * https://datatracker.ietf.org/doc/html/rfc6750
41  * 
9dc286 42  * weitere Info-Links
7ecde3 43  * https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/
U 44  * https://swagger.io/docs/specification/authentication/bearer-authentication/
45  * 
46  * @author Ulrich Hilger
47  * @version 1, 08.06.2021
48  */
49 public class BearerAuthenticator extends Authenticator {
50   
51   /** Der Logger dieser Klasse */
52   private static final Logger logger = Logger.getLogger(BearerAuthenticator.class.getName());
53   
54   public static final String STR_SLASH = "/";
55   public static final String STR_BLANK = " ";
56   public static final String STR_COMMA = ",";
57   public static final String STR_EMPTY = "";
58   public static final String STR_EQUAL = "=";
59   public static final String STR_QUOTE = "\"";
60   
61   public static final String AUTHORIZATION = "Authorization";
62   public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
63   
64   public static final String BEARER = "Bearer";
65   public static final String REALM = "Realm";
66   public static final String ERROR = "error";
67   public static final String ERROR_DESC = "error_description";
68   
69   public static final String MSG_INVALID_TOKEN = "invalid_token";
70   public static final String MSG_TOKEN_EXPIRED = "The access token expired";
71   
72   /** Status code Unauthorized (401) */
73   public static final int SC_UNAUTHORIZED = 401;
74   
75   private Realm realm;
76   
77   private String wwwAuthRealm;
78   
79   private String principalAuthRealm;
80   
81   /** der Schluessel zur Signatur von Tokens */
82   protected final Key key;
83   
84   private long expireSeconds;
85   
86   private long refreshSeconds;
87   
88   private long refreshExpire;
89
90   public BearerAuthenticator() {
91     key = Keys.secretKeyFor(SignatureAlgorithm.HS256);  
92   }
93   
94   @Override
95   public Result authenticate(HttpExchange exchange) {
96     logger.info(exchange.getRequestURI().toString());
97     String jwt = getToken(exchange);
98     if(jwt.equals(STR_EMPTY)) {
99       return unauthorized(exchange);
100     } else {
101       try {
102         Claims body = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt).getBody();
103         Date issueDate = body.getIssuedAt();
104         Date refreshDate = Date.from(issueDate.toInstant().plusSeconds(refreshSeconds));
105         Date now = new Date();
106         if(now.before(refreshDate)) {
107           String jwtUserId = body.getSubject();
108           try {
109             HttpPrincipal pp = new HttpPrincipal(jwtUserId, getPrincipalAuthRealm(exchange));
110             Result result = new Authenticator.Success(pp);
111             return result;
112           } catch (Exception ex) {
113             logger.log(Level.SEVERE, null, ex);
114             return new Authenticator.Failure(SC_UNAUTHORIZED);
115           }
116         } else {
117           return unauthorizedExpired(exchange);
118         }
119       } catch (JwtException ex) {
120         // we *cannot* use the JWT as intended by its creator
121         // z.B. Expiration Date ueberschritten oder Key passt nicht
122         //sessions.remove(jwt);
123         return unauthorized(exchange);
124       }        
125     }
126   }
127   
128   /**
129    * Anmelden
130    * 
131    * @param userId  die Kennung des Benutzers
132    * @param password  das Kennwort des Benutzers
133    * @return Token oder null, wenn die Anmeldung misslang
134    */
8c1928 135   public LoginResponse login(HttpExchange e, String userId, String password) {
7ecde3 136     if (realm.isValid(userId, password)) {
8c1928 137       logger.info(userId + " logged in from IP." + e.getRemoteAddress());
7ecde3 138       LoginResponse r = new LoginResponse();
U 139       String token = createToken(userId, expireSeconds);
140       r.setToken(token);
141       r.setRefreshToken(createToken(userId, refreshExpire));
142       r.setExpiresIn(expireSeconds);
143       return r;
144     } else {
8c1928 145       logger.info("Invalid log in attempt for " + userId + " from IP " + e.getRemoteAddress());
7ecde3 146       return null;
U 147     }
148   }
149   
150   public LoginResponse refresh(String refreshToken) {
151     String userId = validateRefreshToken(refreshToken);
152     if (userId != null) {
153       LoginResponse r = new LoginResponse();
154       String token = createToken(userId, expireSeconds);
155       r.setToken(token);
156       r.setRefreshToken(createToken(userId, refreshExpire));
157       r.setExpiresIn(expireSeconds);
158       return r;
159     } else {
160       return null;
161     }
162   }
163
164   /**
165    * 
166    * Hinweis: Die Methode setExpiration des JWT laesst einen Token 
167    * am entsprechenden Zeitpunkt ungueltig werden. Ein Lesen des Token 
168    * laeuft dann auf einen Fehler und man kann nicht ermitteln, ob der 
169    * Fehler wegen des Ablaufs des Token oder aus anderem Grund entstand.
170    * 
171    * Eine Gueltigkeitsdauer, die bei Ablauf einen Refresh des Tokens 
172    * siganlisieren soll, kann nicht mit diesem Ablaufdatum realisiert werden.
173    * 
174    * @param userId
175    * @return 
176    */
177   private String createToken(String userId, long expire) {
178     Date now = new Date();
179     Date exp = Date.from(now.toInstant().plusSeconds(expire));       
180     String jws = Jwts
181             .builder()
182             .setSubject(userId)
183             .setIssuedAt(now)
184             .setExpiration(exp)
185             .signWith(key)
186             .compact();
187     return jws;
188   }
189   
190   /**
191    * Bis auf weiteres wird hier der Token nur darauf geprueft, ob er ein 
192    * gueltiger JWT ist, der mit dem Schluessel dieses Authenticators 
193    * erzeugt wurde.
194    * 
195    * Evtl. wird es in Zukunft noch noetig, weitere Kriterien einzubauen, 
196    * z.B. ob er zu einem Token aussgegeben wurde, der noch gilt.
197    * 
198    * @param refreshToken
199    * @return die Benutzerkennung aus dem Refresh Token, wenn der 
200    * Refresh Token fuer einen Token Refresh akzeptiert wird, null wenn nicht.
201    */
202   public String validateRefreshToken(String refreshToken) {
203     try {
204       Claims body = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken).getBody();
205       String jwtUserId = body.getSubject();
206       return jwtUserId;
207     } catch (JwtException ex) {
208       // we *cannot* use the JWT as intended by its creator
209       // z.B. Expiration Date ueberschritten oder Key passt nicht
210       //sessions.remove(jwt);
211       return null;
212     }        
213   }
214   
215   /**
216    * Den Token aus dem Authorization Header lesen
217    * 
218    * z.B. Authorization: Bearer mF_9.B5f-4.1JqM
219    * 
220    * @param exchange
221    * @return der Token oder STR_EMPTY, falls 
222    * kein Token gefunden wurde
223    */
224   private String getToken(HttpExchange exchange) {
225     String token = STR_EMPTY;
226     Headers headers = exchange.getRequestHeaders();
227     String auth = headers.getFirst(AUTHORIZATION);
228     if(auth != null) {
229       String[] parts = auth.split(BEARER);
230       if(parts != null && parts.length > 1) {
231         token = parts[1].trim();
232       }
a9b01c 233     } else {
U 234       // unschoen, aber fuer Image-Links in HTML-Inhalten
235       // mit Query versuchen
236       // z.B.
237       //   GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
238       //   Host: server.example.com
239       String query = exchange.getRequestURI().getQuery();
240       if(query != null && query.toLowerCase().contains("access_token")) {
241         String[] parts = query.split("&");
242         for(String part : parts) {
243           String[] keyVal = part.split("=");
244           if(keyVal[0].equalsIgnoreCase("access_token")) {
245             token = keyVal[1].trim();
246           }
247         }
248       }
7ecde3 249     }
U 250     return token;
251   }
252   
253   /**
254    * Den Eintrag fuer das 'realm'-Attribut 
255    * im WWW-Authenticate Header bestimmen
256    * 
257    * @param exchange 
9dc286 258    * @return  den Ausdruck fuer den WWW-Authenticate Header 
7ecde3 259    */
U 260   protected String getWWWAuthRealm(HttpExchange exchange) {
261     return wwwAuthRealm;
262   }
263   
264   /**
265    * Den Namen des Realms bestimmen, wie er fuer authentifizierte Benutzer 
266    * vom Principal ausgegeben wird
267    * 
268    * @param exchange
269    * @return  den Namen des Realms 
270    */
271   protected String getPrincipalAuthRealm(HttpExchange exchange) {
272     return principalAuthRealm;
273   }
274   
275   /**
276    * Wenn die Anfrage eine Token enthaelt, der gemaess setRefreshSeconds 
277    * abgelaufen ist und einen Refresh erfordert.
278    * 
279    *  HTTP/1.1 401 Unauthorized
280    *  WWW-Authenticate: Bearer realm="example",
281    *                    error="invalid_token",
282    *                    error_description="The access token expired"
283    * 
284    * @param exchange
285    * @return 
286    */
287   protected Result unauthorizedExpired(HttpExchange exchange) {
288     StringBuilder sb = new StringBuilder();
289     sb.append(BEARER);
290     sb.append(STR_BLANK);
291     sb.append(REALM);
292     sb.append(STR_EQUAL);
293     sb.append(STR_QUOTE);
294     sb.append(getWWWAuthRealm(exchange));
295     sb.append(STR_QUOTE);
296     sb.append(STR_COMMA);
297     sb.append(STR_BLANK);
298     sb.append(ERROR);
299     sb.append(STR_EQUAL);
300     sb.append(STR_QUOTE);
301     sb.append(MSG_INVALID_TOKEN);
302     sb.append(STR_QUOTE);
303     sb.append(STR_COMMA);
304     sb.append(STR_BLANK);
305     sb.append(ERROR_DESC);
306     sb.append(STR_EQUAL);
307     sb.append(STR_QUOTE);
308     sb.append(MSG_TOKEN_EXPIRED);
309     sb.append(STR_QUOTE);
310     Headers headers = exchange.getResponseHeaders();
311     headers.add(WWW_AUTHENTICATE, sb.toString());
312     HttpResponder r = new HttpResponder();
313     try {
314       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
315     } catch (IOException ex) {
316       logger.log(Level.SEVERE, null, ex);
317     }
318     return new Authenticator.Retry(SC_UNAUTHORIZED);
319   }
320   
321   /**
322    * Wenn die Anfrage keinen Token enthaelt
323    * 
324    * HTTP/1.1 401 Unauthorized
325    * WWW-Authenticate: Bearer realm="example"
326    * 
327    * @param exchange
9dc286 328    * @return das Ergebnis
7ecde3 329    */
U 330   protected Result unauthorized(HttpExchange exchange) {
331     StringBuilder sb = new StringBuilder();
332     sb.append(BEARER);
333     sb.append(STR_BLANK);
334     sb.append(REALM);
335     sb.append(STR_EQUAL);
336     sb.append(STR_QUOTE);
337     sb.append(getWWWAuthRealm(exchange));
338     sb.append(STR_QUOTE);
339     Headers headers = exchange.getResponseHeaders();
340     headers.add(WWW_AUTHENTICATE, sb.toString());
341     HttpResponder r = new HttpResponder();
342     try {
343       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
344     } catch (IOException ex) {
345       logger.log(Level.SEVERE, null, ex);
346     }
347     return new Authenticator.Retry(SC_UNAUTHORIZED);
348   }
349   
a4bee5 350   /**
U 351    * Den Realm dieses Authenticators setzen
352    * @param realm der Realm
353    */
7ecde3 354   public void setRealm(Realm realm) {
U 355     this.realm = realm;
356   }
357   
a4bee5 358   /**
U 359    * Pruefen, ob ein Nutzer eine Rolle hat
360    * @param userId die Kennung des Nutzers
361    * @param roleId  die Rollen-ID des Nutzers
362    * @return true, wenn der Nutzer die Rolle hat, false wenn nicht
363    */
6b3fc6 364   public boolean hasRole(String userId, String roleId) {
U 365     return realm.hasRole(userId, roleId);
b9d3a1 366   }
U 367   
a4bee5 368   /**
U 369    * Den Eintrag fuer das 'realm'-Attribut 
370    * zur Nutzung im WWW-Authenticate Header setzen
371    * 
372    * @param wwwAuthRealm der Text fuer das realm-Attribut im 
373    * WWW-Autehnticate-Header
374    */
7ecde3 375   public void setWWWAuthRealm(String wwwAuthRealm) {
U 376     this.wwwAuthRealm = wwwAuthRealm;
377   }
378   
a4bee5 379   /**
U 380    * Den Namen des Realms setzen, wie er fuer authentifizierte Benutzer 
381    * vom Principal ausgegeben werden soll
382    * 
383    * @param principalAuthRealm der Name des Realms fuer authentifizierte 
384    * Benutzer
385    */
7ecde3 386   public void setPrincipalAuthRealm(String principalAuthRealm) {
U 387     this.principalAuthRealm = principalAuthRealm;
388   }
389   
a4bee5 390   /**
U 391    * Die Dauer der Gueltigkeit einer Authentifizierung in Sekunden
392    * @param seconds die Sekunden, nach denen die Authentifizierung 
393    * ungueltig wird
394    */
7ecde3 395   public void setExpireSeconds(long seconds) {
U 396     this.expireSeconds = seconds;
397   }
398   
a4bee5 399   /**
U 400    * Die Dauer bis eine Authentifizierung eine Erneuerung benoetigt in Sekunden
401    * @param seconds die Sekunden, nach denen die Authentifizierung 
402    * eine Erneuerung benoetigt
403    */
7ecde3 404   public void setRefreshSeconds(long seconds) {
U 405     this.refreshSeconds = seconds;
406   }
407   
a4bee5 408   /**
U 409    * Die Dauer der Gueltigkeit eines Refresh-Token in Sekunden
410    * 
411    * @param seconds die Anzhal Sekunden, die ein Refresh-Token gueltig ist
412    */
7ecde3 413   public void setRefreshExpireSeconds(long seconds) {
U 414     this.refreshExpire = seconds;
415   }
416 }