OAuth-Unterstuetzung fuer jdk.httpserver
ulrich
2021-06-08 7ecde3f6cd2c516dfa5ae8a3380fa92859e72d21
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  * 
40  * 
41  * "The OAuth 2.0 Authorization Framework: Bearer Token Usage"
42  * https://datatracker.ietf.org/doc/html/rfc6750
43  * 
44  * 
45  * https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/
46  * https://swagger.io/docs/specification/authentication/bearer-authentication/
47  * 
48  * @author Ulrich Hilger
49  * @version 1, 08.06.2021
50  */
51 public class BearerAuthenticator extends Authenticator {
52   
53   /** Der Logger dieser Klasse */
54   private static final Logger logger = Logger.getLogger(BearerAuthenticator.class.getName());
55   
56   public static final String STR_SLASH = "/";
57   public static final String STR_BLANK = " ";
58   public static final String STR_COMMA = ",";
59   public static final String STR_EMPTY = "";
60   public static final String STR_EQUAL = "=";
61   public static final String STR_QUOTE = "\"";
62   
63   public static final String AUTHORIZATION = "Authorization";
64   public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
65   
66   public static final String BEARER = "Bearer";
67   public static final String REALM = "Realm";
68   public static final String ERROR = "error";
69   public static final String ERROR_DESC = "error_description";
70   
71   public static final String MSG_INVALID_TOKEN = "invalid_token";
72   public static final String MSG_TOKEN_EXPIRED = "The access token expired";
73   
74   /** Status code Unauthorized (401) */
75   public static final int SC_UNAUTHORIZED = 401;
76   
77   private Realm realm;
78   
79   private String wwwAuthRealm;
80   
81   private String principalAuthRealm;
82   
83   /** der Schluessel zur Signatur von Tokens */
84   protected final Key key;
85   
86   private long expireSeconds;
87   
88   private long refreshSeconds;
89   
90   private long refreshExpire;
91
92   public BearerAuthenticator() {
93     key = Keys.secretKeyFor(SignatureAlgorithm.HS256);  
94   }
95   
96   @Override
97   public Result authenticate(HttpExchange exchange) {
98     logger.info(exchange.getRequestURI().toString());
99     String jwt = getToken(exchange);
100     if(jwt.equals(STR_EMPTY)) {
101       return unauthorized(exchange);
102     } else {
103       try {
104         Claims body = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt).getBody();
105         Date issueDate = body.getIssuedAt();
106         Date refreshDate = Date.from(issueDate.toInstant().plusSeconds(refreshSeconds));
107         Date now = new Date();
108         if(now.before(refreshDate)) {
109           String jwtUserId = body.getSubject();
110           try {
111             HttpPrincipal pp = new HttpPrincipal(jwtUserId, getPrincipalAuthRealm(exchange));
112             Result result = new Authenticator.Success(pp);
113             return result;
114           } catch (Exception ex) {
115             logger.log(Level.SEVERE, null, ex);
116             return new Authenticator.Failure(SC_UNAUTHORIZED);
117           }
118         } else {
119           return unauthorizedExpired(exchange);
120         }
121       } catch (JwtException ex) {
122         // we *cannot* use the JWT as intended by its creator
123         // z.B. Expiration Date ueberschritten oder Key passt nicht
124         //sessions.remove(jwt);
125         return unauthorized(exchange);
126       }        
127     }
128   }
129   
130   /**
131    * Anmelden
132    * 
133    * @param userId  die Kennung des Benutzers
134    * @param password  das Kennwort des Benutzers
135    * @return Token oder null, wenn die Anmeldung misslang
136    */
137   public LoginResponse login(String userId, String password) {
138     if (realm.isValid(userId, password)) {
139       LoginResponse r = new LoginResponse();
140       String token = createToken(userId, expireSeconds);
141       r.setToken(token);
142       r.setRefreshToken(createToken(userId, refreshExpire));
143       r.setExpiresIn(expireSeconds);
144       return r;
145     } else {
146       return null;
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       }
233     }
234     return token;
235   }
236   
237   /**
238    * Den Eintrag fuer das 'realm'-Attribut 
239    * im WWW-Authenticate Header bestimmen
240    * 
241    * @param exchange 
242    */
243   protected String getWWWAuthRealm(HttpExchange exchange) {
244     return wwwAuthRealm;
245   }
246   
247   /**
248    * Den Namen des Realms bestimmen, wie er fuer authentifizierte Benutzer 
249    * vom Principal ausgegeben wird
250    * 
251    * @param exchange
252    * @return  den Namen des Realms 
253    */
254   protected String getPrincipalAuthRealm(HttpExchange exchange) {
255     return principalAuthRealm;
256   }
257   
258   /**
259    * Wenn die Anfrage eine Token enthaelt, der gemaess setRefreshSeconds 
260    * abgelaufen ist und einen Refresh erfordert.
261    * 
262    *  HTTP/1.1 401 Unauthorized
263    *  WWW-Authenticate: Bearer realm="example",
264    *                    error="invalid_token",
265    *                    error_description="The access token expired"
266    * 
267    * @param exchange
268    * @return 
269    */
270   protected Result unauthorizedExpired(HttpExchange exchange) {
271     StringBuilder sb = new StringBuilder();
272     sb.append(BEARER);
273     sb.append(STR_BLANK);
274     sb.append(REALM);
275     sb.append(STR_EQUAL);
276     sb.append(STR_QUOTE);
277     sb.append(getWWWAuthRealm(exchange));
278     sb.append(STR_QUOTE);
279     sb.append(STR_COMMA);
280     sb.append(STR_BLANK);
281     sb.append(ERROR);
282     sb.append(STR_EQUAL);
283     sb.append(STR_QUOTE);
284     sb.append(MSG_INVALID_TOKEN);
285     sb.append(STR_QUOTE);
286     sb.append(STR_COMMA);
287     sb.append(STR_BLANK);
288     sb.append(ERROR_DESC);
289     sb.append(STR_EQUAL);
290     sb.append(STR_QUOTE);
291     sb.append(MSG_TOKEN_EXPIRED);
292     sb.append(STR_QUOTE);
293     Headers headers = exchange.getResponseHeaders();
294     headers.add(WWW_AUTHENTICATE, sb.toString());
295     HttpResponder r = new HttpResponder();
296     try {
297       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
298     } catch (IOException ex) {
299       logger.log(Level.SEVERE, null, ex);
300     }
301     return new Authenticator.Retry(SC_UNAUTHORIZED);
302   }
303   
304   /**
305    * Wenn die Anfrage keinen Token enthaelt
306    * 
307    * HTTP/1.1 401 Unauthorized
308    * WWW-Authenticate: Bearer realm="example"
309    * 
310    * @param exchange
311    * @return 
312    * @throws java.io.IOException 
313    */
314   protected Result unauthorized(HttpExchange exchange) {
315     StringBuilder sb = new StringBuilder();
316     sb.append(BEARER);
317     sb.append(STR_BLANK);
318     sb.append(REALM);
319     sb.append(STR_EQUAL);
320     sb.append(STR_QUOTE);
321     sb.append(getWWWAuthRealm(exchange));
322     sb.append(STR_QUOTE);
323     Headers headers = exchange.getResponseHeaders();
324     headers.add(WWW_AUTHENTICATE, sb.toString());
325     HttpResponder r = new HttpResponder();
326     try {
327       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
328     } catch (IOException ex) {
329       logger.log(Level.SEVERE, null, ex);
330     }
331     return new Authenticator.Retry(SC_UNAUTHORIZED);
332   }
333   
334   public void setRealm(Realm realm) {
335     this.realm = realm;
336   }
337   
338   public void setWWWAuthRealm(String wwwAuthRealm) {
339     this.wwwAuthRealm = wwwAuthRealm;
340   }
341   
342   public void setPrincipalAuthRealm(String principalAuthRealm) {
343     this.principalAuthRealm = principalAuthRealm;
344   }
345   
346   public void setExpireSeconds(long seconds) {
347     this.expireSeconds = seconds;
348   }
349   
350   public void setRefreshSeconds(long seconds) {
351     this.refreshSeconds = seconds;
352   }
353   
354   public void setRefreshExpireSeconds(long seconds) {
355     this.refreshExpire = seconds;
356   }
357 }