OAuth-Unterstuetzung fuer jdk.httpserver
ulrich
2021-06-15 b9d3a1ab5d34949386c89e6d04ebde33dec63787
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    */
135   public LoginResponse login(String userId, String password) {
136     if (realm.isValid(userId, password)) {
137       LoginResponse r = new LoginResponse();
138       String token = createToken(userId, expireSeconds);
139       r.setToken(token);
140       r.setRefreshToken(createToken(userId, refreshExpire));
141       r.setExpiresIn(expireSeconds);
142       return r;
143     } else {
144       return null;
145     }
146   }
147   
148   public LoginResponse refresh(String refreshToken) {
149     String userId = validateRefreshToken(refreshToken);
150     if (userId != null) {
151       LoginResponse r = new LoginResponse();
152       String token = createToken(userId, expireSeconds);
153       r.setToken(token);
154       r.setRefreshToken(createToken(userId, refreshExpire));
155       r.setExpiresIn(expireSeconds);
156       return r;
157     } else {
158       return null;
159     }
160   }
161
162   /**
163    * 
164    * Hinweis: Die Methode setExpiration des JWT laesst einen Token 
165    * am entsprechenden Zeitpunkt ungueltig werden. Ein Lesen des Token 
166    * laeuft dann auf einen Fehler und man kann nicht ermitteln, ob der 
167    * Fehler wegen des Ablaufs des Token oder aus anderem Grund entstand.
168    * 
169    * Eine Gueltigkeitsdauer, die bei Ablauf einen Refresh des Tokens 
170    * siganlisieren soll, kann nicht mit diesem Ablaufdatum realisiert werden.
171    * 
172    * @param userId
173    * @return 
174    */
175   private String createToken(String userId, long expire) {
176     Date now = new Date();
177     Date exp = Date.from(now.toInstant().plusSeconds(expire));       
178     String jws = Jwts
179             .builder()
180             .setSubject(userId)
181             .setIssuedAt(now)
182             .setExpiration(exp)
183             .signWith(key)
184             .compact();
185     return jws;
186   }
187   
188   /**
189    * Bis auf weiteres wird hier der Token nur darauf geprueft, ob er ein 
190    * gueltiger JWT ist, der mit dem Schluessel dieses Authenticators 
191    * erzeugt wurde.
192    * 
193    * Evtl. wird es in Zukunft noch noetig, weitere Kriterien einzubauen, 
194    * z.B. ob er zu einem Token aussgegeben wurde, der noch gilt.
195    * 
196    * @param refreshToken
197    * @return die Benutzerkennung aus dem Refresh Token, wenn der 
198    * Refresh Token fuer einen Token Refresh akzeptiert wird, null wenn nicht.
199    */
200   public String validateRefreshToken(String refreshToken) {
201     try {
202       Claims body = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken).getBody();
203       String jwtUserId = body.getSubject();
204       return jwtUserId;
205     } catch (JwtException ex) {
206       // we *cannot* use the JWT as intended by its creator
207       // z.B. Expiration Date ueberschritten oder Key passt nicht
208       //sessions.remove(jwt);
209       return null;
210     }        
211   }
212   
213   /**
214    * Den Token aus dem Authorization Header lesen
215    * 
216    * z.B. Authorization: Bearer mF_9.B5f-4.1JqM
217    * 
218    * @param exchange
219    * @return der Token oder STR_EMPTY, falls 
220    * kein Token gefunden wurde
221    */
222   private String getToken(HttpExchange exchange) {
223     String token = STR_EMPTY;
224     Headers headers = exchange.getRequestHeaders();
225     String auth = headers.getFirst(AUTHORIZATION);
226     if(auth != null) {
227       String[] parts = auth.split(BEARER);
228       if(parts != null && parts.length > 1) {
229         token = parts[1].trim();
230       }
a9b01c 231     } else {
U 232       // unschoen, aber fuer Image-Links in HTML-Inhalten
233       // mit Query versuchen
234       // z.B.
235       //   GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
236       //   Host: server.example.com
237       String query = exchange.getRequestURI().getQuery();
238       if(query != null && query.toLowerCase().contains("access_token")) {
239         String[] parts = query.split("&");
240         for(String part : parts) {
241           String[] keyVal = part.split("=");
242           if(keyVal[0].equalsIgnoreCase("access_token")) {
243             token = keyVal[1].trim();
244           }
245         }
246       }
7ecde3 247     }
U 248     return token;
249   }
250   
251   /**
252    * Den Eintrag fuer das 'realm'-Attribut 
253    * im WWW-Authenticate Header bestimmen
254    * 
255    * @param exchange 
9dc286 256    * @return  den Ausdruck fuer den WWW-Authenticate Header 
7ecde3 257    */
U 258   protected String getWWWAuthRealm(HttpExchange exchange) {
259     return wwwAuthRealm;
260   }
261   
262   /**
263    * Den Namen des Realms bestimmen, wie er fuer authentifizierte Benutzer 
264    * vom Principal ausgegeben wird
265    * 
266    * @param exchange
267    * @return  den Namen des Realms 
268    */
269   protected String getPrincipalAuthRealm(HttpExchange exchange) {
270     return principalAuthRealm;
271   }
272   
273   /**
274    * Wenn die Anfrage eine Token enthaelt, der gemaess setRefreshSeconds 
275    * abgelaufen ist und einen Refresh erfordert.
276    * 
277    *  HTTP/1.1 401 Unauthorized
278    *  WWW-Authenticate: Bearer realm="example",
279    *                    error="invalid_token",
280    *                    error_description="The access token expired"
281    * 
282    * @param exchange
283    * @return 
284    */
285   protected Result unauthorizedExpired(HttpExchange exchange) {
286     StringBuilder sb = new StringBuilder();
287     sb.append(BEARER);
288     sb.append(STR_BLANK);
289     sb.append(REALM);
290     sb.append(STR_EQUAL);
291     sb.append(STR_QUOTE);
292     sb.append(getWWWAuthRealm(exchange));
293     sb.append(STR_QUOTE);
294     sb.append(STR_COMMA);
295     sb.append(STR_BLANK);
296     sb.append(ERROR);
297     sb.append(STR_EQUAL);
298     sb.append(STR_QUOTE);
299     sb.append(MSG_INVALID_TOKEN);
300     sb.append(STR_QUOTE);
301     sb.append(STR_COMMA);
302     sb.append(STR_BLANK);
303     sb.append(ERROR_DESC);
304     sb.append(STR_EQUAL);
305     sb.append(STR_QUOTE);
306     sb.append(MSG_TOKEN_EXPIRED);
307     sb.append(STR_QUOTE);
308     Headers headers = exchange.getResponseHeaders();
309     headers.add(WWW_AUTHENTICATE, sb.toString());
310     HttpResponder r = new HttpResponder();
311     try {
312       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
313     } catch (IOException ex) {
314       logger.log(Level.SEVERE, null, ex);
315     }
316     return new Authenticator.Retry(SC_UNAUTHORIZED);
317   }
318   
319   /**
320    * Wenn die Anfrage keinen Token enthaelt
321    * 
322    * HTTP/1.1 401 Unauthorized
323    * WWW-Authenticate: Bearer realm="example"
324    * 
325    * @param exchange
9dc286 326    * @return das Ergebnis
7ecde3 327    */
U 328   protected Result unauthorized(HttpExchange exchange) {
329     StringBuilder sb = new StringBuilder();
330     sb.append(BEARER);
331     sb.append(STR_BLANK);
332     sb.append(REALM);
333     sb.append(STR_EQUAL);
334     sb.append(STR_QUOTE);
335     sb.append(getWWWAuthRealm(exchange));
336     sb.append(STR_QUOTE);
337     Headers headers = exchange.getResponseHeaders();
338     headers.add(WWW_AUTHENTICATE, sb.toString());
339     HttpResponder r = new HttpResponder();
340     try {
341       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
342     } catch (IOException ex) {
343       logger.log(Level.SEVERE, null, ex);
344     }
345     return new Authenticator.Retry(SC_UNAUTHORIZED);
346   }
347   
348   public void setRealm(Realm realm) {
349     this.realm = realm;
350   }
351   
b9d3a1 352   public Realm getRealm() {
U 353     return realm;
354   }
355   
7ecde3 356   public void setWWWAuthRealm(String wwwAuthRealm) {
U 357     this.wwwAuthRealm = wwwAuthRealm;
358   }
359   
360   public void setPrincipalAuthRealm(String principalAuthRealm) {
361     this.principalAuthRealm = principalAuthRealm;
362   }
363   
364   public void setExpireSeconds(long seconds) {
365     this.expireSeconds = seconds;
366   }
367   
368   public void setRefreshSeconds(long seconds) {
369     this.refreshSeconds = seconds;
370   }
371   
372   public void setRefreshExpireSeconds(long seconds) {
373     this.refreshExpire = seconds;
374   }
375 }