New file |
| | |
| | | /* |
| | | http-oauth - OAuth Extensions to jdk.httpserver |
| | | Copyright (C) 2021 Ulrich Hilger |
| | | |
| | | This program is free software: you can redistribute it and/or modify |
| | | it under the terms of the GNU Affero General Public License as |
| | | published by the Free Software Foundation, either version 3 of the |
| | | License, or (at your option) any later version. |
| | | |
| | | This program is distributed in the hope that it will be useful, |
| | | but WITHOUT ANY WARRANTY; without even the implied warranty of |
| | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| | | GNU Affero General Public License for more details. |
| | | |
| | | You should have received a copy of the GNU Affero General Public License |
| | | along with this program. If not, see <https://www.gnu.org/licenses/>. |
| | | */ |
| | | package de.uhilger.httpserver.oauth; |
| | | |
| | | import com.sun.net.httpserver.Authenticator; |
| | | import com.sun.net.httpserver.Headers; |
| | | import com.sun.net.httpserver.HttpExchange; |
| | | import com.sun.net.httpserver.HttpPrincipal; |
| | | import de.uhilger.httpserver.auth.realm.Realm; |
| | | import de.uhilger.httpserver.base.handler.HttpResponder; |
| | | import io.jsonwebtoken.Claims; |
| | | import io.jsonwebtoken.JwtException; |
| | | import io.jsonwebtoken.Jwts; |
| | | import io.jsonwebtoken.SignatureAlgorithm; |
| | | import io.jsonwebtoken.security.Keys; |
| | | import java.io.IOException; |
| | | import java.security.Key; |
| | | import java.util.Date; |
| | | import java.util.logging.Level; |
| | | import java.util.logging.Logger; |
| | | |
| | | /** |
| | | * Die Klasse Authenticator authentifziert gemäß OAuth-Spezifikation |
| | | * |
| | | * |
| | | * "The OAuth 2.0 Authorization Framework: Bearer Token Usage" |
| | | * https://datatracker.ietf.org/doc/html/rfc6750 |
| | | * |
| | | * |
| | | * https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/ |
| | | * https://swagger.io/docs/specification/authentication/bearer-authentication/ |
| | | * |
| | | * @author Ulrich Hilger |
| | | * @version 1, 08.06.2021 |
| | | */ |
| | | public class BearerAuthenticator extends Authenticator { |
| | | |
| | | /** Der Logger dieser Klasse */ |
| | | private static final Logger logger = Logger.getLogger(BearerAuthenticator.class.getName()); |
| | | |
| | | public static final String STR_SLASH = "/"; |
| | | public static final String STR_BLANK = " "; |
| | | public static final String STR_COMMA = ","; |
| | | public static final String STR_EMPTY = ""; |
| | | public static final String STR_EQUAL = "="; |
| | | public static final String STR_QUOTE = "\""; |
| | | |
| | | public static final String AUTHORIZATION = "Authorization"; |
| | | public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; |
| | | |
| | | public static final String BEARER = "Bearer"; |
| | | public static final String REALM = "Realm"; |
| | | public static final String ERROR = "error"; |
| | | public static final String ERROR_DESC = "error_description"; |
| | | |
| | | public static final String MSG_INVALID_TOKEN = "invalid_token"; |
| | | public static final String MSG_TOKEN_EXPIRED = "The access token expired"; |
| | | |
| | | /** Status code Unauthorized (401) */ |
| | | public static final int SC_UNAUTHORIZED = 401; |
| | | |
| | | private Realm realm; |
| | | |
| | | private String wwwAuthRealm; |
| | | |
| | | private String principalAuthRealm; |
| | | |
| | | /** der Schluessel zur Signatur von Tokens */ |
| | | protected final Key key; |
| | | |
| | | private long expireSeconds; |
| | | |
| | | private long refreshSeconds; |
| | | |
| | | private long refreshExpire; |
| | | |
| | | public BearerAuthenticator() { |
| | | key = Keys.secretKeyFor(SignatureAlgorithm.HS256); |
| | | } |
| | | |
| | | @Override |
| | | public Result authenticate(HttpExchange exchange) { |
| | | logger.info(exchange.getRequestURI().toString()); |
| | | String jwt = getToken(exchange); |
| | | if(jwt.equals(STR_EMPTY)) { |
| | | return unauthorized(exchange); |
| | | } else { |
| | | try { |
| | | Claims body = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt).getBody(); |
| | | Date issueDate = body.getIssuedAt(); |
| | | Date refreshDate = Date.from(issueDate.toInstant().plusSeconds(refreshSeconds)); |
| | | Date now = new Date(); |
| | | if(now.before(refreshDate)) { |
| | | String jwtUserId = body.getSubject(); |
| | | try { |
| | | HttpPrincipal pp = new HttpPrincipal(jwtUserId, getPrincipalAuthRealm(exchange)); |
| | | Result result = new Authenticator.Success(pp); |
| | | return result; |
| | | } catch (Exception ex) { |
| | | logger.log(Level.SEVERE, null, ex); |
| | | return new Authenticator.Failure(SC_UNAUTHORIZED); |
| | | } |
| | | } else { |
| | | return unauthorizedExpired(exchange); |
| | | } |
| | | } catch (JwtException ex) { |
| | | // we *cannot* use the JWT as intended by its creator |
| | | // z.B. Expiration Date ueberschritten oder Key passt nicht |
| | | //sessions.remove(jwt); |
| | | return unauthorized(exchange); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Anmelden |
| | | * |
| | | * @param userId die Kennung des Benutzers |
| | | * @param password das Kennwort des Benutzers |
| | | * @return Token oder null, wenn die Anmeldung misslang |
| | | */ |
| | | public LoginResponse login(String userId, String password) { |
| | | if (realm.isValid(userId, password)) { |
| | | LoginResponse r = new LoginResponse(); |
| | | String token = createToken(userId, expireSeconds); |
| | | r.setToken(token); |
| | | r.setRefreshToken(createToken(userId, refreshExpire)); |
| | | r.setExpiresIn(expireSeconds); |
| | | return r; |
| | | } else { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | public LoginResponse refresh(String refreshToken) { |
| | | String userId = validateRefreshToken(refreshToken); |
| | | if (userId != null) { |
| | | LoginResponse r = new LoginResponse(); |
| | | String token = createToken(userId, expireSeconds); |
| | | r.setToken(token); |
| | | r.setRefreshToken(createToken(userId, refreshExpire)); |
| | | r.setExpiresIn(expireSeconds); |
| | | return r; |
| | | } else { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * |
| | | * Hinweis: Die Methode setExpiration des JWT laesst einen Token |
| | | * am entsprechenden Zeitpunkt ungueltig werden. Ein Lesen des Token |
| | | * laeuft dann auf einen Fehler und man kann nicht ermitteln, ob der |
| | | * Fehler wegen des Ablaufs des Token oder aus anderem Grund entstand. |
| | | * |
| | | * Eine Gueltigkeitsdauer, die bei Ablauf einen Refresh des Tokens |
| | | * siganlisieren soll, kann nicht mit diesem Ablaufdatum realisiert werden. |
| | | * |
| | | * @param userId |
| | | * @return |
| | | */ |
| | | private String createToken(String userId, long expire) { |
| | | Date now = new Date(); |
| | | Date exp = Date.from(now.toInstant().plusSeconds(expire)); |
| | | String jws = Jwts |
| | | .builder() |
| | | .setSubject(userId) |
| | | .setIssuedAt(now) |
| | | .setExpiration(exp) |
| | | .signWith(key) |
| | | .compact(); |
| | | return jws; |
| | | } |
| | | |
| | | /** |
| | | * Bis auf weiteres wird hier der Token nur darauf geprueft, ob er ein |
| | | * gueltiger JWT ist, der mit dem Schluessel dieses Authenticators |
| | | * erzeugt wurde. |
| | | * |
| | | * Evtl. wird es in Zukunft noch noetig, weitere Kriterien einzubauen, |
| | | * z.B. ob er zu einem Token aussgegeben wurde, der noch gilt. |
| | | * |
| | | * @param refreshToken |
| | | * @return die Benutzerkennung aus dem Refresh Token, wenn der |
| | | * Refresh Token fuer einen Token Refresh akzeptiert wird, null wenn nicht. |
| | | */ |
| | | public String validateRefreshToken(String refreshToken) { |
| | | try { |
| | | Claims body = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken).getBody(); |
| | | String jwtUserId = body.getSubject(); |
| | | return jwtUserId; |
| | | } catch (JwtException ex) { |
| | | // we *cannot* use the JWT as intended by its creator |
| | | // z.B. Expiration Date ueberschritten oder Key passt nicht |
| | | //sessions.remove(jwt); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Den Token aus dem Authorization Header lesen |
| | | * |
| | | * z.B. Authorization: Bearer mF_9.B5f-4.1JqM |
| | | * |
| | | * @param exchange |
| | | * @return der Token oder STR_EMPTY, falls |
| | | * kein Token gefunden wurde |
| | | */ |
| | | private String getToken(HttpExchange exchange) { |
| | | String token = STR_EMPTY; |
| | | Headers headers = exchange.getRequestHeaders(); |
| | | String auth = headers.getFirst(AUTHORIZATION); |
| | | if(auth != null) { |
| | | String[] parts = auth.split(BEARER); |
| | | if(parts != null && parts.length > 1) { |
| | | token = parts[1].trim(); |
| | | } |
| | | } |
| | | return token; |
| | | } |
| | | |
| | | /** |
| | | * Den Eintrag fuer das 'realm'-Attribut |
| | | * im WWW-Authenticate Header bestimmen |
| | | * |
| | | * @param exchange |
| | | */ |
| | | protected String getWWWAuthRealm(HttpExchange exchange) { |
| | | return wwwAuthRealm; |
| | | } |
| | | |
| | | /** |
| | | * Den Namen des Realms bestimmen, wie er fuer authentifizierte Benutzer |
| | | * vom Principal ausgegeben wird |
| | | * |
| | | * @param exchange |
| | | * @return den Namen des Realms |
| | | */ |
| | | protected String getPrincipalAuthRealm(HttpExchange exchange) { |
| | | return principalAuthRealm; |
| | | } |
| | | |
| | | /** |
| | | * Wenn die Anfrage eine Token enthaelt, der gemaess setRefreshSeconds |
| | | * abgelaufen ist und einen Refresh erfordert. |
| | | * |
| | | * HTTP/1.1 401 Unauthorized |
| | | * WWW-Authenticate: Bearer realm="example", |
| | | * error="invalid_token", |
| | | * error_description="The access token expired" |
| | | * |
| | | * @param exchange |
| | | * @return |
| | | */ |
| | | protected Result unauthorizedExpired(HttpExchange exchange) { |
| | | StringBuilder sb = new StringBuilder(); |
| | | sb.append(BEARER); |
| | | sb.append(STR_BLANK); |
| | | sb.append(REALM); |
| | | sb.append(STR_EQUAL); |
| | | sb.append(STR_QUOTE); |
| | | sb.append(getWWWAuthRealm(exchange)); |
| | | sb.append(STR_QUOTE); |
| | | sb.append(STR_COMMA); |
| | | sb.append(STR_BLANK); |
| | | sb.append(ERROR); |
| | | sb.append(STR_EQUAL); |
| | | sb.append(STR_QUOTE); |
| | | sb.append(MSG_INVALID_TOKEN); |
| | | sb.append(STR_QUOTE); |
| | | sb.append(STR_COMMA); |
| | | sb.append(STR_BLANK); |
| | | sb.append(ERROR_DESC); |
| | | sb.append(STR_EQUAL); |
| | | sb.append(STR_QUOTE); |
| | | sb.append(MSG_TOKEN_EXPIRED); |
| | | sb.append(STR_QUOTE); |
| | | Headers headers = exchange.getResponseHeaders(); |
| | | headers.add(WWW_AUTHENTICATE, sb.toString()); |
| | | HttpResponder r = new HttpResponder(); |
| | | try { |
| | | r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY); |
| | | } catch (IOException ex) { |
| | | logger.log(Level.SEVERE, null, ex); |
| | | } |
| | | return new Authenticator.Retry(SC_UNAUTHORIZED); |
| | | } |
| | | |
| | | /** |
| | | * Wenn die Anfrage keinen Token enthaelt |
| | | * |
| | | * HTTP/1.1 401 Unauthorized |
| | | * WWW-Authenticate: Bearer realm="example" |
| | | * |
| | | * @param exchange |
| | | * @return |
| | | * @throws java.io.IOException |
| | | */ |
| | | protected Result unauthorized(HttpExchange exchange) { |
| | | StringBuilder sb = new StringBuilder(); |
| | | sb.append(BEARER); |
| | | sb.append(STR_BLANK); |
| | | sb.append(REALM); |
| | | sb.append(STR_EQUAL); |
| | | sb.append(STR_QUOTE); |
| | | sb.append(getWWWAuthRealm(exchange)); |
| | | sb.append(STR_QUOTE); |
| | | Headers headers = exchange.getResponseHeaders(); |
| | | headers.add(WWW_AUTHENTICATE, sb.toString()); |
| | | HttpResponder r = new HttpResponder(); |
| | | try { |
| | | r.antwortSenden(exchange, SC_UNAUTHORIZED, STR_EMPTY); |
| | | } catch (IOException ex) { |
| | | logger.log(Level.SEVERE, null, ex); |
| | | } |
| | | return new Authenticator.Retry(SC_UNAUTHORIZED); |
| | | } |
| | | |
| | | public void setRealm(Realm realm) { |
| | | this.realm = realm; |
| | | } |
| | | |
| | | public void setWWWAuthRealm(String wwwAuthRealm) { |
| | | this.wwwAuthRealm = wwwAuthRealm; |
| | | } |
| | | |
| | | public void setPrincipalAuthRealm(String principalAuthRealm) { |
| | | this.principalAuthRealm = principalAuthRealm; |
| | | } |
| | | |
| | | public void setExpireSeconds(long seconds) { |
| | | this.expireSeconds = seconds; |
| | | } |
| | | |
| | | public void setRefreshSeconds(long seconds) { |
| | | this.refreshSeconds = seconds; |
| | | } |
| | | |
| | | public void setRefreshExpireSeconds(long seconds) { |
| | | this.refreshExpire = seconds; |
| | | } |
| | | } |