diff --git a/pom.xml b/pom.xml
index 4d1da651..8981ec5a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,7 +77,12 @@
spring-boot-starter-security
${spring_boot_version}
-
+
+
+
+
+
+
org.springframework.boot
@@ -309,10 +314,10 @@
-
- ch.qos.logback
- logback-classic
-
+
+
+
+
@@ -543,6 +548,25 @@
${logback-classic.version}
+
+ io.jsonwebtoken
+ jjwt
+ 0.9.1
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+
+ com.nimbusds
+ nimbus-jose-jwt
+ 9.37.3
+
+
+
diff --git a/src/main/database/create-mysql-database.sql b/src/main/database/create-mysql-database.sql
index 1430f952..80da5021 100644
--- a/src/main/database/create-mysql-database.sql
+++ b/src/main/database/create-mysql-database.sql
@@ -49,4 +49,5 @@ CREATE TABLE `tenant` (
--insert into user_access (org_id, access_name, access_key) values (1, 'Mercy', 'password1234');
--insert into user_access (org_id, access_name, access_key) values (2, 'Bob', '1234password1234');
--insert into user_access (org_id, access_name, access_key) values (3, 'DEFAULT', 'BabySharkJaws');
---insert into user_access (org_id, access_name, access_key) values (4, 'Connectathon', 'SundaysR0ck!');
\ No newline at end of file
+insert into user_access (org_id, access_name, access_key) values (4, 'Connectathon', 'SundaysR0ck!');
+insert into user_access (org_id, access_name, access_key) values (4, 'admin', '?whooosM0reSorino?);
\ No newline at end of file
diff --git a/src/main/java/org/immregistries/iis/kernal/JwtAuthProvider.java b/src/main/java/org/immregistries/iis/kernal/JwtAuthProvider.java
new file mode 100644
index 00000000..e456a060
--- /dev/null
+++ b/src/main/java/org/immregistries/iis/kernal/JwtAuthProvider.java
@@ -0,0 +1,120 @@
+package org.immregistries.iis.kernal;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.ECDSAVerifier;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jwt.SignedJWT;
+import org.hibernate.Session;
+import org.immregistries.iis.kernal.fhir.security.ServletHelper;
+import org.immregistries.iis.kernal.model.UserAccess;
+import org.immregistries.iis.kernal.servlet.PopServlet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.security.PublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.ParseException;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController()
+public class JwtAuthProvider {
+ @Autowired
+ JwtUtils jwtUtils;
+ Map keystore;
+ Map jwtStore;
+ Logger logger = LoggerFactory.getLogger(JwtAuthProvider.class);
+
+ public JwtAuthProvider() {
+ this.keystore = new HashMap<>(10);
+ this.jwtStore = new HashMap<>(10);
+ }
+
+ @GetMapping("/.well-known/smart-configuration")
+ public String wellKnownConfiguration() {
+ return "{\n" +
+ " \"token_endpoint\": \"/token\",\n" +
+ " \"token_endpoint_auth_methods_supported\": [\"private_key_jwt\"],\n" +
+ " \"token_endpoint_auth_signing_alg_values_supported\": [\"RS384\", \"ES384\"],\n" +
+ " \"scopes_supported\": [\"system/*.rs\"]\n" +
+ "}";
+ }
+
+ @PostMapping("/registerClient")
+ public String register(@RequestBody String jwkString) { //TODO TLS config
+ // assert(ServletHelper.getUserAccess().getAccessName().equals("admin")); TODO safely define admin user
+ logger.info("Registering client JWK: {}", jwkString); //TODO not log key
+ Map parsedJwk = new Gson().fromJson(jwkString, new TypeToken>() {}.getType());
+ String alg = (String) parsedJwk.get("alg");
+ String kid = (String) parsedJwk.get("kid");
+ switch (alg) {
+ case "RS384":
+ RSAPublicKey rsaJwk = new Gson().fromJson(jwkString, RSAPublicKey.class);
+ keystore.put(kid,rsaJwk); // TODO change key ?
+ break;
+ case "ES384" :
+ ECPublicKey ecJwk = new Gson().fromJson(jwkString, ECPublicKey.class);
+ keystore.put(kid,ecJwk); // TODO change key ?
+ break;
+ default:
+ throw new RuntimeException("Unsupported Algorithm");
+ }
+ return "ok";
+ }
+
+ /**
+ * Only allowed for connectathons users
+ */
+ @PostMapping("/token")
+ public String smartJwtAuth(@RequestParam String client_assertion_type, @RequestParam String client_assertion) throws ParseException, JOSEException {
+ SignedJWT signedJWT = SignedJWT.parse(client_assertion);
+ /**
+ * Reading Jwt Headers
+ */
+ String alg = signedJWT.getHeader().getAlgorithm().getName();
+ String typ = signedJWT.getHeader().getType().getType();
+ String kid = signedJWT.getHeader().getKeyID();
+// String jku = signedJWT.getHeader().getJWKURL(); not supported
+
+ assert (typ.equals("JWT"));
+ assert (keystore.containsKey(kid));
+ JWSVerifier verifier;
+ switch (alg) {
+ case "RS384":
+ verifier = new RSASSAVerifier((RSAPublicKey) keystore.get(kid));
+ break;
+ case "ES384" :
+ verifier = new ECDSAVerifier((ECPublicKey) keystore.get(kid));
+ break;
+ default:
+ throw new RuntimeException("Unsupported Algorithm");
+ }
+ assert(signedJWT.verify(verifier));
+ assert(signedJWT.getJWTClaimsSet().getClaim("jti") != null);
+ /**
+ * check that the jti value has not been previously encountered for the given iss within the maximum allowed authentication JWT lifetime (e.g., 5 minutes). This check prevents replay attacks.
+ */
+ SignedJWT olderJwt = jwtStore.get((String) signedJWT.getJWTClaimsSet().getClaim("jti"));
+ if (olderJwt != null && olderJwt.verify(verifier) && olderJwt.getJWTClaimsSet().getClaim("iss").equals(signedJWT.getJWTClaimsSet().getClaim("iss")) && ((long) olderJwt.getJWTClaimsSet().getClaim("exp")) < System.currentTimeMillis()) {
+ throw new RuntimeException("Token already used");
+ }
+ jwtStore.put((String) signedJWT.getJWTClaimsSet().getClaim("jti"), signedJWT);
+ Session dataSession = null;
+ try {
+ dataSession = PopServlet.getDataSession();
+ UserAccess userAccess = ServletHelper.authenticateUserAccessUsernamePassword("Connectathon","Connectathon",dataSession);
+ return jwtUtils.generateJwtToken(userAccess);
+ } finally {
+ if (dataSession != null) {
+ dataSession.close();
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/immregistries/iis/kernal/JwtUtils.java b/src/main/java/org/immregistries/iis/kernal/JwtUtils.java
new file mode 100644
index 00000000..9e25565d
--- /dev/null
+++ b/src/main/java/org/immregistries/iis/kernal/JwtUtils.java
@@ -0,0 +1,49 @@
+package org.immregistries.iis.kernal;
+
+import io.jsonwebtoken.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+@Component
+public class JwtUtils {
+ private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
+ @Value("${iis.api.app.jwtSecret}")
+ private String jwtSecret;
+ @Value("${iis.api.app.jwtExpirationMs}")
+ private int jwtExpirationMs;
+ public String generateJwtToken(Authentication authentication) {
+ UserDetails userPrincipal = (UserDetails) authentication.getPrincipal();
+ return Jwts.builder()
+ .setSubject((userPrincipal.getUsername()))
+ .setIssuedAt(new Date())
+ .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
+ .signWith(SignatureAlgorithm.HS512, jwtSecret)
+ .compact();
+ }
+ public String getUserNameFromJwtToken(String token) {
+ return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
+ }
+ public boolean validateJwtToken(String authToken) {
+ try {
+ Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
+ return true;
+ } catch (SignatureException e) {
+ logger.error("Invalid JWT signature: {}", e.getMessage());
+ } catch (MalformedJwtException e) {
+ logger.error("Invalid JWT token: {}", e.getMessage());
+ } catch (ExpiredJwtException e) {
+ logger.error("JWT token is expired: {}", e.getMessage());
+ } catch (UnsupportedJwtException e) {
+ logger.error("JWT token is unsupported: {}", e.getMessage());
+ } catch (IllegalArgumentException e) {
+ logger.error("JWT claims string is empty: {}", e.getMessage());
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/immregistries/iis/kernal/fhir/security/ServerSecurityConfig.java b/src/main/java/org/immregistries/iis/kernal/fhir/security/ServerSecurityConfig.java
index b48e176e..58fc3825 100644
--- a/src/main/java/org/immregistries/iis/kernal/fhir/security/ServerSecurityConfig.java
+++ b/src/main/java/org/immregistries/iis/kernal/fhir/security/ServerSecurityConfig.java
@@ -24,7 +24,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, CustomOAuthSuccessHand
.antMatchers(HttpMethod.GET, "/", "/home", "/pop", "/SubscriptionTopic/**", "/img/**").permitAll()
.antMatchers("/loginForm", "/oauth2/**", "/login" ).permitAll()
// API AUTHORIZATION AND AUTHENTICATION SEPARATED
- .antMatchers("/fhir/**", "/soap").permitAll()
+ .antMatchers("/fhir/**", "/soap","/.well-known/smart-configuration","/registerClient", "/token").permitAll()
.anyRequest().authenticated()
.and()
//USERNAME
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 0567f46d..cd9c7159 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -11,6 +11,12 @@ management:
server:
servlet:
contextPath: "/iis"
+iis:
+ api:
+ app:
+ secret: "LeVentNousPorterasT0utDizp4ra1tra"
+ jwtExpirationMs: 30000
+# TODO change secret
spring:
security:
oauth2: