Bearer Token Authentifizierung fuer neon
ulrich
2024-02-20 177043dea9f4efe18cea2ee7864ddb7b25c4646a
commit | author | age
177043 1 /*
U 2   neon-auth - Authentication Extensions to Neon
3   Copyright (C) 2024  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.neon.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.neon.HttpResponder;
25 import io.jsonwebtoken.Claims;
26 import io.jsonwebtoken.JwtException;
27 import io.jsonwebtoken.Jwts;
28 import io.jsonwebtoken.SignatureAlgorithm;
29 import io.jsonwebtoken.security.Keys;
30 import java.io.IOException;
31 import java.security.Key;
32 import java.util.Date;
33 import java.util.Map;
34
35 /**
36  * Die Klasse Authenticator authentifziert gem&auml;&szlig; OAuth-Spezifikation 
37  * "The OAuth 2.0 Authorization Framework: Bearer Token Usage"
38  * https://datatracker.ietf.org/doc/html/rfc6750
39  * 
40  * weitere Info-Links
41  * https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/
42  * https://swagger.io/docs/specification/authentication/bearer-authentication/
43  * 
44  * @author Ulrich Hilger
45  * @version 1, 08.06.2021
46  */
47 public class BearerAuthenticator extends Authenticator /* implements Element*/ {
48   
49   public static final String STR_SLASH = "/";
50   public static final String STR_BLANK = " ";
51   public static final String STR_COMMA = ",";
52   public static final String STR_EMPTY = "";
53   public static final String STR_EQUAL = "=";
54   public static final String STR_QUOTE = "\"";
55   
56   public static final String AUTHORIZATION = "Authorization";
57   public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
58   
59   public static final String BEARER = "Bearer";
60   public static final String REALM = "Realm";
61   public static final String ERROR = "error";
62   public static final String ERROR_DESC = "error_description";
63   
64   public static final String MSG_INVALID_TOKEN = "invalid_token";
65   public static final String MSG_TOKEN_EXPIRED = "The access token expired";
66   
67   /** Status code Unauthorized (401) */
68   public static final int SC_UNAUTHORIZED = 401;
69   
70   /** der Schluessel zur Signatur von Tokens */
71   protected final Key key;
72   
73   public BearerAuthenticator() {
74     key = Keys.secretKeyFor(SignatureAlgorithm.HS256);  
75   }  
76   
77   @Override
78   public Result authenticate(HttpExchange exchange) {
79     Map attributes = exchange.getHttpContext().getAttributes();
80     String login = (String) attributes.getOrDefault("loginRoute", "/login");
81     String refresh = (String) attributes.getOrDefault("refreshRoute", "/refresh");
82     String route = exchange.getRequestURI().getPath().substring(exchange.getHttpContext().getPath().length());
83     if(route.endsWith(login) || route.endsWith(refresh)) {
84       Result result = new Authenticator.Success(new HttpPrincipal("login", "realm"));
85       return result;
86     } else {
87       String jwt = getToken(exchange);
88       if (jwt.equals(STR_EMPTY)) {
89         return unauthorized(exchange);
90       } else {
91         try {
92           Claims body = Jwts
93                   .parserBuilder()
94                   .setSigningKey(key)
95                   .build()
96                   .parseClaimsJws(jwt)
97                   .getBody();
98           Date issueDate = body.getIssuedAt();
99           Date refreshDate = Date.from(issueDate
100                           .toInstant()
101                           .plusSeconds(Long.parseLong(
102                             (String) attributes.getOrDefault("refreshSeconds", "3600"))));
103           Date now = new Date();
104           if (now.before(refreshDate)) {
105             String jwtUserId = body.getSubject();
106             try {
107               HttpPrincipal pp = new HttpPrincipal(jwtUserId, "realm");
108               Result result = new Authenticator.Success(pp);
109               return result;
110             } catch (Exception ex) {
111               return new Authenticator.Failure(SC_UNAUTHORIZED);
112             }
113           } else {
114             return unauthorizedExpired(exchange);
115           }
116         } catch (JwtException ex) {
117           // we *cannot* use the JWT as intended by its creator
118           // z.B. Expiration Date ueberschritten oder Key passt nicht
119           //sessions.remove(jwt);
120           return unauthorized(exchange);
121         }
122       }
123     }
124   }
125   
126   /**
127    * 
128    * Hinweis: Die Methode setExpiration des JWT laesst einen Token 
129    * am entsprechenden Zeitpunkt ungueltig werden. Ein Lesen des Token 
130    * laeuft dann auf einen Fehler und man kann nicht ermitteln, ob der 
131    * Fehler wegen des Ablaufs des Token oder aus anderem Grund entstand.
132    * 
133    * Eine Gueltigkeitsdauer, die bei Ablauf einen Refresh des Tokens 
134    * siganlisieren soll, kann nicht mit diesem Ablaufdatum realisiert werden.
135    * 
136    * @param userId
137    * @return 
138    */
139   public String createToken(String userId, long expire) {
140     Date now = new Date();
141     Date exp = Date.from(now.toInstant().plusSeconds(expire));       
142     String jws = Jwts
143             .builder()
144             .setSubject(userId)
145             .setIssuedAt(now)
146             .setExpiration(exp)
147             .signWith(key)
148             .compact();
149     return jws;
150   }
151   
152   /**
153    * Bis auf weiteres wird hier der Token nur darauf geprueft, ob er ein 
154    * gueltiger JWT ist, der mit dem Schluessel dieses Authenticators 
155    * erzeugt wurde.
156    * 
157    * Evtl. wird es in Zukunft noch noetig, weitere Kriterien einzubauen, 
158    * z.B. ob er zu einem Token aussgegeben wurde, der noch gilt.
159    * 
160    * @param refreshToken
161    * @return die Benutzerkennung aus dem Refresh Token, wenn der 
162    * Refresh Token fuer einen Token Refresh akzeptiert wird, null wenn nicht.
163    */
164   public String validateRefreshToken(String refreshToken) {
165     try {
166       Claims body = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken).getBody();
167       String jwtUserId = body.getSubject();
168       return jwtUserId;
169     } catch (JwtException ex) {
170       // we *cannot* use the JWT as intended by its creator
171       // z.B. Expiration Date ueberschritten oder Key passt nicht
172       //sessions.remove(jwt);
173       return null;
174     }        
175   }
176   
177   /**
178    * Den Token aus dem Authorization Header lesen
179    * 
180    * z.B. Authorization: Bearer mF_9.B5f-4.1JqM
181    * 
182    * @param exchange
183    * @return der Token oder STR_EMPTY, falls 
184    * kein Token gefunden wurde
185    */
186   private String getToken(HttpExchange exchange) {
187     String token = STR_EMPTY;
188     Headers headers = exchange.getRequestHeaders();
189     String auth = headers.getFirst(AUTHORIZATION);
190     if(auth != null) {
191       String[] parts = auth.split(BEARER);
192       if(parts != null && parts.length > 1) {
193         token = parts[1].trim();
194       }
195     } else {
196       // unschoen, aber fuer Image-Links in HTML-Inhalten
197       // mit Query versuchen
198       // z.B.
199       //   GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
200       //   Host: server.example.com
201       String query = exchange.getRequestURI().getQuery();
202       if(query != null && query.toLowerCase().contains("access_token")) {
203         String[] parts = query.split("&");
204         for(String part : parts) {
205           String[] keyVal = part.split("=");
206           if(keyVal[0].equalsIgnoreCase("access_token")) {
207             token = keyVal[1].trim();
208           }
209         }
210       }
211     }
212     return token;
213   }
214   
215   /**
216    * Wenn die Anfrage eine Token enthaelt, der gemaess setRefreshSeconds 
217    * abgelaufen ist und einen Refresh erfordert.
218    * 
219    *  HTTP/1.1 401 Unauthorized
220    *  WWW-Authenticate: Bearer realm="example",
221    *                    error="invalid_token",
222    *                    error_description="The access token expired"
223    * 
224    * @param exchange
225    * @return 
226    */
227   protected Result unauthorizedExpired(HttpExchange exchange) {
228     StringBuilder sb = new StringBuilder();
229     sb.append(BEARER);
230     sb.append(STR_BLANK);
231     sb.append(REALM);
232     sb.append(STR_EQUAL);
233     sb.append(STR_QUOTE);
234     //sb.append(getWWWAuthRealm(exchange));
235     sb.append(exchange.getHttpContext().getPath());
236     sb.append(STR_QUOTE);
237     sb.append(STR_COMMA);
238     sb.append(STR_BLANK);
239     sb.append(ERROR);
240     sb.append(STR_EQUAL);
241     sb.append(STR_QUOTE);
242     sb.append(MSG_INVALID_TOKEN);
243     sb.append(STR_QUOTE);
244     sb.append(STR_COMMA);
245     sb.append(STR_BLANK);
246     sb.append(ERROR_DESC);
247     sb.append(STR_EQUAL);
248     sb.append(STR_QUOTE);
249     sb.append(MSG_TOKEN_EXPIRED);
250     sb.append(STR_QUOTE);
251     Headers headers = exchange.getResponseHeaders();
252     headers.add(WWW_AUTHENTICATE, sb.toString());
253     HttpResponder r = new HttpResponder();
254     try {
255       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
256     } catch (IOException ex) {
257       //logger.log(Level.SEVERE, null, ex);
258     }
259     return new Authenticator.Retry(SC_UNAUTHORIZED);
260   }
261   
262   /**
263    * Wenn die Anfrage keinen Token enthaelt
264    * 
265    * HTTP/1.1 401 Unauthorized
266    * WWW-Authenticate: Bearer realm="example"
267    * 
268    * @param exchange
269    * @return das Ergebnis
270    */
271   protected Result unauthorized(HttpExchange exchange) {
272     StringBuilder sb = new StringBuilder();
273     sb.append(BEARER);
274     sb.append(STR_BLANK);
275     sb.append(REALM);
276     sb.append(STR_EQUAL);
277     sb.append(STR_QUOTE);
278     //sb.append(getWWWAuthRealm(exchange));
279     sb.append(exchange.getHttpContext().getPath());
280     sb.append(STR_QUOTE);
281     Headers headers = exchange.getResponseHeaders();
282     headers.add(WWW_AUTHENTICATE, sb.toString());
283     HttpResponder r = new HttpResponder();
284     try {
285       r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY);
286     } catch (IOException ex) {
287       //logger.log(Level.SEVERE, null, ex);
288     }
289     return new Authenticator.Retry(SC_UNAUTHORIZED);
290   }
291 }