diff --git a/docs-web-common/pom.xml b/docs-web-common/pom.xml index c1ff6b9e..9cbd5bb2 100644 --- a/docs-web-common/pom.xml +++ b/docs-web-common/pom.xml @@ -68,6 +68,27 @@ org.slf4j jul-to-slf4j + + + io.github.openfeign + feign-okhttp + 13.0 + + + io.github.openfeign + feign-gson + 13.0 + + + io.github.openfeign + feign-slf4j + 13.0 + + + com.auth0 + java-jwt + 4.4.0 + diff --git a/docs-web-common/src/main/java/com/sismics/feign/KeycloakClient.java b/docs-web-common/src/main/java/com/sismics/feign/KeycloakClient.java new file mode 100644 index 00000000..ba5b5efe --- /dev/null +++ b/docs-web-common/src/main/java/com/sismics/feign/KeycloakClient.java @@ -0,0 +1,10 @@ +package com.sismics.feign; + +import com.sismics.feign.model.KeycloakCertKeys; +import feign.RequestLine; + +public interface KeycloakClient { + + @RequestLine("GET /protocol/openid-connect/certs") + KeycloakCertKeys getCert(); +} diff --git a/docs-web-common/src/main/java/com/sismics/feign/model/KeycloakCertKey.java b/docs-web-common/src/main/java/com/sismics/feign/model/KeycloakCertKey.java new file mode 100644 index 00000000..ef25544b --- /dev/null +++ b/docs-web-common/src/main/java/com/sismics/feign/model/KeycloakCertKey.java @@ -0,0 +1,27 @@ +package com.sismics.feign.model; + +import java.util.List; + +public class KeycloakCertKey { + public String kid; + public List x5c; + + public KeycloakCertKey() { + } + + public List getX5c() { + return x5c; + } + + public void setX5c(List x5c) { + this.x5c = x5c; + } + + public String getKid() { + return kid; + } + + public void setKid(String kid) { + this.kid = kid; + } +} diff --git a/docs-web-common/src/main/java/com/sismics/feign/model/KeycloakCertKeys.java b/docs-web-common/src/main/java/com/sismics/feign/model/KeycloakCertKeys.java new file mode 100644 index 00000000..8cf387e1 --- /dev/null +++ b/docs-web-common/src/main/java/com/sismics/feign/model/KeycloakCertKeys.java @@ -0,0 +1,18 @@ +package com.sismics.feign.model; + +import java.util.List; + +public class KeycloakCertKeys { + public List keys; + + public KeycloakCertKeys() { + } + + public List getKeys() { + return keys; + } + + public void setKeys(List keys) { + this.keys = keys; + } +} diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/JwtBasedSecurityFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/JwtBasedSecurityFilter.java new file mode 100644 index 00000000..f7ece132 --- /dev/null +++ b/docs-web-common/src/main/java/com/sismics/util/filter/JwtBasedSecurityFilter.java @@ -0,0 +1,131 @@ +package com.sismics.util.filter; + +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.impl.JWTParser; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; +import java.util.Base64; +import com.sismics.docs.core.constant.Constants; +import com.sismics.docs.core.dao.UserDao; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.feign.KeycloakClient; +import feign.Feign; +import feign.gson.GsonDecoder; +import feign.gson.GsonEncoder; +import feign.okhttp.OkHttpClient; +import feign.slf4j.Slf4jLogger; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.interfaces.RSAPublicKey; +import java.util.Objects; +import java.util.UUID; + +import static java.util.Optional.ofNullable; + +/** + * This filter is used to authenticate the user having an active session by validating a jwt token. + * The filter extracts the jwt token stored from Authorization header. + * It validates the token by calling an Identity Broker like KeyCloak. + * If validated, the user is retrieved, and the filter injects a UserPrincipal into the request attribute. + * + * @author smitra + */ +public class JwtBasedSecurityFilter extends SecurityFilter { + private static final Logger log = LoggerFactory.getLogger(JwtBasedSecurityFilter.class); + /** + * Name of the cookie used to store the authentication token. + */ + public static final String HEADER_NAME = "Authorization"; + + @Override + protected User authenticate(final HttpServletRequest request) { + log.info("Jwt authentication started"); + User user = null; + String token = extractAuthToken(request).replace("Bearer ", ""); + DecodedJWT jwt = JWT.decode(token); + if (verifyJwt(jwt, token)) { + String email = jwt.getClaim("preferred_username").toString(); + UserDao userDao = new UserDao(); + user = userDao.getActiveByUsername(email); + if (user == null) { + user = new User(); + user.setRoleId(Constants.DEFAULT_USER_ROLE); + user.setUsername(email); + user.setEmail(email); + user.setStorageQuota(10L); + user.setPassword(UUID.randomUUID().toString()); + try { + userDao.create(user, email); + log.info("user created"); + } catch (Exception e) { + log.info("Error:" + e.getMessage()); + return null; + } + } + } + return user; + } + + private boolean verifyJwt(final DecodedJWT jwt, final String token) { + + try { + buildJWTVerifier(jwt).verify(token); + // if token is valid no exception will be thrown + log.info("Valid TOKEN"); + return Boolean.TRUE; + } catch (CertificateException e) { + //if CertificateException comes from buildJWTVerifier() + log.info("InValid TOKEN"); + e.printStackTrace(); + return Boolean.FALSE; + } catch (JWTVerificationException e) { + // if JWT Token in invalid + log.info("InValid TOKEN"); + e.printStackTrace(); + return Boolean.FALSE; + } catch (Exception e) { + // If any other exception comes + log.info("InValid TOKEN, Exception Occurred"); + e.printStackTrace(); + return Boolean.FALSE; + } + } + + private String extractAuthToken(final HttpServletRequest request) { + return ofNullable(request.getHeader("Authorization")).orElse(""); + } + + private RSAPublicKey getPublicKey(DecodedJWT jwt) { + KeycloakClient client = Feign.builder() + .client(new OkHttpClient()) + .encoder(new GsonEncoder()) + .decoder(new GsonDecoder()) + .logLevel(feign.Logger.Level.BASIC) + .logger(new Slf4jLogger(KeycloakClient.class)) + .target(KeycloakClient.class, jwt.getIssuer()); + String publicKey = client.getCert().getKeys().stream().filter(k -> Objects.equals(k.getKid(), jwt.getKeyId())) + .findFirst() + .map(k -> k.getX5c().get(0)) + .orElse(""); + try { + var decode = Base64.getDecoder().decode(publicKey); + var certificate = CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(decode)); + return (RSAPublicKey)certificate.getPublicKey(); + } catch (CertificateException ex) { + return null; + } + } + + private JWTVerifier buildJWTVerifier(DecodedJWT jwt) throws CertificateException { + var algo = Algorithm.RSA256(getPublicKey(jwt), null); + return JWT.require(algo).build(); + } +} diff --git a/docs-web/src/main/webapp/WEB-INF/web.xml b/docs-web/src/main/webapp/WEB-INF/web.xml index 720b328e..e5c06e24 100644 --- a/docs-web/src/main/webapp/WEB-INF/web.xml +++ b/docs-web/src/main/webapp/WEB-INF/web.xml @@ -44,6 +44,12 @@ true + + jwtBasedSecurityFilter + com.sismics.util.filter.JwtBasedSecurityFilter + true + + headerBasedSecurityFilter com.sismics.util.filter.HeaderBasedSecurityFilter @@ -59,6 +65,11 @@ /api/* + + jwtBasedSecurityFilter + /api/* + + headerBasedSecurityFilter /api/*