OAuth-Unterstuetzung fuer jdk.httpserver
ulrich
2021-06-08 9dc2865a7408c33f403056f408690c227bdfe690
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;
25 import de.uhilger.httpserver.base.handler.HttpResponder;
26 import io.jsonwebtoken.Claims;
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       }
231     }
232     return token;
233   }
234   
235   /**
236    * Den Eintrag fuer das 'realm'-Attribut 
237    * im WWW-Authenticate Header bestimmen
238    * 
239    * @param exchange 
9dc286 240    * @return  den Ausdruck fuer den WWW-Authenticate Header 
7ecde3 241    */
U 242   protected String getWWWAuthRealm(HttpExchange exchange) {
243     return wwwAuthRealm;
244   }
245   
246   /**
247    * Den Namen des Realms bestimmen, wie er fuer authentifizierte Benutzer 
248    * vom Principal ausgegeben wird
249    * 
250    * @param exchange
251    * @return  den Namen des Realms 
252    */
253   protected String getPrincipalAuthRealm(HttpExchange exchange) {
254     return principalAuthRealm;
255   }
256   
257   /**
258    * Wenn die Anfrage eine Token enthaelt, der gemaess setRefreshSeconds 
259    * abgelaufen ist und einen Refresh erfordert.
260    * 
261    *  HTTP/1.1 401 Unauthorized
262    *  WWW-Authenticate: Bearer realm="example",
263    *                    error="invalid_token",
264    *                    error_description="The access token expired"
265    * 
266    * @param exchange
267    * @return 
268    */
269   protected Result unauthorizedExpired(HttpExchange exchange) {
270     StringBuilder sb = new StringBuilder();
271     sb.append(BEARER);
272     sb.append(STR_BLANK);
273     sb.append(REALM);
274     sb.append(STR_EQUAL);
275     sb.append(STR_QUOTE);
276     sb.append(getWWWAuthRealm(exchange));
277     sb.append(STR_QUOTE);
278     sb.append(STR_COMMA);
279     sb.append(STR_BLANK);
280     sb.append(ERROR);
281     sb.append(STR_EQUAL);
282     sb.append(STR_QUOTE);
283     sb.append(MSG_INVALID_TOKEN);
284     sb.append(STR_QUOTE);
285     sb.append(STR_COMMA);
286     sb.append(STR_BLANK);
287     sb.append(ERROR_DESC);
288     sb.append(STR_EQUAL);
289     sb.append(STR_QUOTE);
290     sb.append(MSG_TOKEN_EXPIRED);
291     sb.append(STR_QUOTE);
292     Headers headers = exchange.getResponseHeaders();
293     headers.add(WWW_AUTHENTICATE, sb.toString());
294     HttpResponder r = new HttpResponder();
295     try {
296       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
297     } catch (IOException ex) {
298       logger.log(Level.SEVERE, null, ex);
299     }
300     return new Authenticator.Retry(SC_UNAUTHORIZED);
301   }
302   
303   /**
304    * Wenn die Anfrage keinen Token enthaelt
305    * 
306    * HTTP/1.1 401 Unauthorized
307    * WWW-Authenticate: Bearer realm="example"
308    * 
309    * @param exchange
9dc286 310    * @return das Ergebnis
7ecde3 311    */
U 312   protected Result unauthorized(HttpExchange exchange) {
313     StringBuilder sb = new StringBuilder();
314     sb.append(BEARER);
315     sb.append(STR_BLANK);
316     sb.append(REALM);
317     sb.append(STR_EQUAL);
318     sb.append(STR_QUOTE);
319     sb.append(getWWWAuthRealm(exchange));
320     sb.append(STR_QUOTE);
321     Headers headers = exchange.getResponseHeaders();
322     headers.add(WWW_AUTHENTICATE, sb.toString());
323     HttpResponder r = new HttpResponder();
324     try {
325       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
326     } catch (IOException ex) {
327       logger.log(Level.SEVERE, null, ex);
328     }
329     return new Authenticator.Retry(SC_UNAUTHORIZED);
330   }
331   
332   public void setRealm(Realm realm) {
333     this.realm = realm;
334   }
335   
336   public void setWWWAuthRealm(String wwwAuthRealm) {
337     this.wwwAuthRealm = wwwAuthRealm;
338   }
339   
340   public void setPrincipalAuthRealm(String principalAuthRealm) {
341     this.principalAuthRealm = principalAuthRealm;
342   }
343   
344   public void setExpireSeconds(long seconds) {
345     this.expireSeconds = seconds;
346   }
347   
348   public void setRefreshSeconds(long seconds) {
349     this.refreshSeconds = seconds;
350   }
351   
352   public void setRefreshExpireSeconds(long seconds) {
353     this.refreshExpire = seconds;
354   }
355 }