OAuth-Unterstuetzung fuer jdk.httpserver
ulrich
2021-06-08 7ecde3f6cd2c516dfa5ae8a3380fa92859e72d21
Erste Fassung der OAuth Bearer Authentication fertig
4 files added
1 files modified
638 ■■■■■ changed files
README.md 2 ●●●●● patch | view | raw | blame | history
src/de/uhilger/httpserver/oauth/BearerAuthenticator.java 357 ●●●●● patch | view | raw | blame | history
src/de/uhilger/httpserver/oauth/BearerLoginHandler.java 142 ●●●●● patch | view | raw | blame | history
src/de/uhilger/httpserver/oauth/BearerRefreshHandler.java 55 ●●●●● patch | view | raw | blame | history
src/de/uhilger/httpserver/oauth/LoginResponse.java 82 ●●●●● patch | view | raw | blame | history
README.md
@@ -10,6 +10,8 @@
* JJWT Gson
* JJWT Impl
* Google Gson
* http-base
* http-realm
## Lizenz
src/de/uhilger/httpserver/oauth/BearerAuthenticator.java
New file
@@ -0,0 +1,357 @@
/*
  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&auml;&szlig; 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;
  }
}
src/de/uhilger/httpserver/oauth/BearerLoginHandler.java
New file
@@ -0,0 +1,142 @@
/*
  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.google.gson.Gson;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import de.uhilger.httpserver.auth.realm.User;
import de.uhilger.httpserver.base.handler.HttpHelper;
import de.uhilger.httpserver.base.handler.HttpResponder;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
 * Ein Login Handler, der zur Authentifizierung ein Objekt der Klasse
 * BearerAuthenticator im HttpContext benoetigt.
 *
 * Der Authenticator wird mit der Methode
 * context.getAttributes().get(ATTR_AUTHENTICATOR);
 * aus dem HttpContext entnommen, d.h., der Authenticator muss zuvor dort
 * eingetragen werden. Das kann wie folgt vonstatten gehen:
 *
 * HttpContext context = server.createContext("/myapp/secure/service", new SomeServiceHandler());
 * BearerApiAuthenticator auth = new BearerAuthenticator();
 * context.setAuthenticator(auth);
 *
 * ...und danach...
 *
 * context = server.createContext("/myapp/login", new BearerLoginHandler());
 * context.getAttributes().put(LoginHandler.ATTR_AUTHENTICATOR, auth);
 *
 * @author Ulrich Hilger
 * @version 1, 08.06.2021
 */
public class BearerLoginHandler implements HttpHandler {
  private static final Logger logger = Logger.getLogger(BearerLoginHandler.class.getName());
  public static final String ATTR_AUTHENTICATOR = "authenticator";
  public static final String CACHE_CONTROL = "Cache-Control";
  public static final String NO_STORE = "no-store";
  public static final String PRAGMA = "Pragma";
  public static final String NO_CACHE = "no-cache";
  public static final String BEARER_CONTENT_TYPE = "application/json;charset=UTF-8";
  /*
    gemaess RFC 6750 lautet die Antwort auf eine erfolgreiche Anmeldung
    wie folgt:
     HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache
     {
       "access_token":"mF_9.B5f-4.1JqM",
       "token_type":"Bearer",
       "expires_in":3600,
       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
     }
  */
  @Override
  public void handle(HttpExchange exchange) throws IOException {
    HttpContext context = exchange.getHttpContext();
    Object o = context.getAttributes().get(ATTR_AUTHENTICATOR);
    if (o instanceof BearerAuthenticator) {
      BearerAuthenticator auth = (BearerAuthenticator) o;
      User user = getUser(exchange);
      LoginResponse response = auth.login(user.getName(), user.getPassword());
      handleLoginResponse(exchange, response);
      /*
      if(response != null) {
        // hier erfolg melden
        // 200 OK
        setLoginHeader(exchange);
        HttpResponder r = new HttpResponder();
        r.antwortSenden(exchange, 200, response.toJson());
      } else {
        HttpResponder r = new HttpResponder();
        r.antwortSenden(exchange, 406, "Login failed.");
      }
      */
    } else {
      HttpResponder r = new HttpResponder();
      r.antwortSenden(exchange, 500, "No suitable authenticator.");
    }
  }
  protected void handleLoginResponse(HttpExchange exchange, LoginResponse response) throws IOException {
    if(response != null) {
      // hier erfolg melden
      // 200 OK
      setLoginHeader(exchange);
      HttpResponder r = new HttpResponder();
      r.antwortSenden(exchange, 200, response.toJson());
    } else {
      HttpResponder r = new HttpResponder();
      r.antwortSenden(exchange, 406, "Login failed.");
    }
  }
  private void setLoginHeader(HttpExchange exchange) {
    Headers headers = exchange.getResponseHeaders();
    headers.add(HttpHelper.CONTENT_TYPE, BEARER_CONTENT_TYPE);
    headers.add(CACHE_CONTROL, NO_STORE);
    headers.add(PRAGMA, NO_CACHE);
  }
  private User getUser(HttpExchange exchange) throws IOException {
    /*
    Wenn ein JSON-Inhalt im Body uebermittelt wird, steht
    dort evtl. etwas wie
    {"name": "fred", "password": "secret"}
    das kann wie folgt gelesen werden
     */
    String body = new HttpHelper().bodyLesen(exchange);
    Gson gson = new Gson();
    User user = gson.fromJson(body, User.class);
    return user;
  }
}
src/de/uhilger/httpserver/oauth/BearerRefreshHandler.java
New file
@@ -0,0 +1,55 @@
/*
  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.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import de.uhilger.httpserver.base.handler.HttpHelper;
import de.uhilger.httpserver.base.handler.HttpResponder;
import static de.uhilger.httpserver.oauth.BearerLoginHandler.ATTR_AUTHENTICATOR;
import java.io.IOException;
/**
 *
 *
 * @author Ulrich Hilger
 * @version 1, 08.06.2021
 */
public class BearerRefreshHandler extends BearerLoginHandler {
  @Override
  public void handle(HttpExchange exchange) throws IOException {
    HttpHelper h = new HttpHelper();
    String body = h.bodyLesen(exchange);
    String[] parts = body.split("&");
    for(String part : parts) {
      String[] keyVals = part.split("=");
      if(keyVals[0].equalsIgnoreCase("refresh_token")) {
        HttpContext context = exchange.getHttpContext();
        Object o = context.getAttributes().get(ATTR_AUTHENTICATOR);
        if (o instanceof BearerAuthenticator) {
          BearerAuthenticator auth = (BearerAuthenticator) o;
          LoginResponse response = auth.refresh(keyVals[1]);
          handleLoginResponse(exchange, response);
        }
      }
    }
  }
}
src/de/uhilger/httpserver/oauth/LoginResponse.java
New file
@@ -0,0 +1,82 @@
/*
  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.google.gson.Gson;
/**
 * Die Klasse LoginResponse modelliert die Antwort auf eine HTTP-Anfrage
 * gemaess Bearer Authentication nach RFC 6750
 *
 * @author Ulrich Hilger
 * @version 1, 08.6.2021
 */
public class LoginResponse {
  /*
       HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache
     {
       "access_token":"mF_9.B5f-4.1JqM",
       "token_type":"Bearer",
       "expires_in":3600,
       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
     }
*/
  private String access_token;
  private final String token_type = BearerAuthenticator.BEARER;
  private long expires_in;
  private String refresh_token;
  public String getToken() {
    return access_token;
  }
  public void setToken(String token) {
    this.access_token = token;
  }
  public String getRefreshToken() {
    return refresh_token;
  }
  public void setRefreshToken(String refreshToken) {
    this.refresh_token = refreshToken;
  }
  public String getTokenType() {
    return token_type;
  }
  public long getExpiresIn() {
    return expires_in;
  }
  public void setExpiresIn(long seconds) {
    this.expires_in = seconds;
  }
  public String toJson() {
    Gson gson = new Gson();
    return gson.toJson(this);
  }
}