Skip to content

Commit

Permalink
SMART Authentication ready for testing on connectathon
Browse files Browse the repository at this point in the history
  • Loading branch information
cerbeor committed Jan 8, 2024
1 parent 66c2025 commit 9e95f46
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 120 deletions.
34 changes: 26 additions & 8 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -550,15 +550,33 @@

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
</exclusions>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>

<!-- <dependency>-->
<!-- <groupId>io.jsonwebtoken</groupId>-->
<!-- <artifactId>jjwt</artifactId>-->
<!-- <version>0.9.1</version>-->
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <groupId>com.fasterxml.jackson.core</groupId>-->
<!-- <artifactId>jackson-databind</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
<groupId>com.nimbusds</groupId>
Expand Down
54 changes: 39 additions & 15 deletions src/main/java/org/immregistries/iis/kernal/JwtAuthProvider.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package org.immregistries.iis.kernal;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEDecrypter;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.crypto.RSADecrypter;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.SignedJWT;
import io.jsonwebtoken.*;
import org.hibernate.Session;
import org.immregistries.iis.kernal.fhir.security.ServletHelper;
import org.immregistries.iis.kernal.model.UserAccess;
Expand All @@ -21,20 +23,23 @@

import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;

import static org.immregistries.iis.kernal.fhir.interceptors.SessionAuthorizationInterceptor.CONNECTATHON_USER;

@RestController()
public class JwtAuthProvider {
@Autowired
JwtUtils jwtUtils;
Map<String, PublicKey> keystore;
Map<String,SignedJWT> jwtStore;
Map<String,String> jwtStore;
Logger logger = LoggerFactory.getLogger(JwtAuthProvider.class);

private final static String CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";

public JwtAuthProvider() {
this.keystore = new HashMap<>(10);
this.jwtStore = new HashMap<>(10);
Expand Down Expand Up @@ -72,8 +77,7 @@ public String register(@RequestBody String jwkString) { //TODO TLS config
default:
throw new RuntimeException("Unsupported Algorithm");
}
logger.info("Registering client key id: {}", parsedJwk.getKeyID() );

logger.info("Registered client key id: {}, {}", parsedJwk.getKeyID(), keystore.size());
} catch (ParseException e) {
throw new RuntimeException(e);
} catch (JOSEException e) {
Expand All @@ -82,11 +86,15 @@ public String register(@RequestBody String jwkString) { //TODO TLS config
return "JWK REGISTERED";
}


/**
* Only allowed for connectathons users
*/
@PostMapping("/token")
public String smartJwtAuth(@RequestParam String client_assertion_type, @RequestParam String client_assertion) throws ParseException, JOSEException {
if (!client_assertion_type.equals(CLIENT_ASSERTION_TYPE) ) {
throw new InvalidRequestException("Unsupported Client Assertion type,supporting only " + CLIENT_ASSERTION_TYPE);
}
SignedJWT signedJWT = SignedJWT.parse(client_assertion);
/**
* Reading Jwt Headers
Expand All @@ -96,9 +104,16 @@ public String smartJwtAuth(@RequestParam String client_assertion_type, @Request
String kid = signedJWT.getHeader().getKeyID();
// String jku = signedJWT.getHeader().getJWKURL(); not supported

assert (typ.equals("JWT"));
assert (keystore.containsKey(kid));
if (!typ.equals("JWT")) {
throw new InvalidRequestException("Unsupported type header,supporting only JWT");
}
if(!keystore.containsKey(kid)) {
throw new InvalidRequestException("Unknown key id " + kid);
}
JWSVerifier verifier;
/**
* Checking supported algorithms
*/
switch (alg) {
case "RS384":
verifier = new RSASSAVerifier((RSAPublicKey) keystore.get(kid));
Expand All @@ -109,20 +124,29 @@ public String smartJwtAuth(@RequestParam String client_assertion_type, @Request
default:
throw new RuntimeException("Unsupported Algorithm");
}
assert(signedJWT.verify(verifier));
assert(signedJWT.getJWTClaimsSet().getClaim("jti") != null);
if (!signedJWT.verify(verifier)) {
throw new InvalidRequestException("Authentication Error");
}
JwtParser jwtParser = Jwts.parser().verifyWith(keystore.get(kid)).build();
Jws<Claims> claimsJws = jwtParser.parseSignedClaims(client_assertion);
if(claimsJws.getPayload().get("jti") == null) {
throw new InvalidRequestException("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");
String olderToken = jwtStore.get((String) claimsJws.getPayload().get("jti"));
if (olderToken != null){
Jws<Claims> olderJwt = jwtParser.parseSignedClaims(olderToken);
if (olderJwt.getPayload().get("iss").equals(claimsJws.getPayload().get("iss")) && ((long) olderJwt.getPayload().get("exp")) < System.currentTimeMillis()) {
throw new RuntimeException("Token already used");
}
}
jwtStore.put((String) signedJWT.getJWTClaimsSet().getClaim("jti"), signedJWT);
jwtStore.put((String) signedJWT.getJWTClaimsSet().getClaim("jti"), client_assertion);
Session dataSession = null;
try {
dataSession = PopServlet.getDataSession();
UserAccess userAccess = ServletHelper.authenticateUserAccessUsernamePassword("Connectathon","Connectathon",dataSession);
UserAccess userAccess = ServletHelper.authenticateUserAccessUsernamePassword(CONNECTATHON_USER,CONNECTATHON_USER,dataSession);
return jwtUtils.generateJwtToken(userAccess);
} finally {
if (dataSession != null) {
Expand Down
89 changes: 54 additions & 35 deletions src/main/java/org/immregistries/iis/kernal/JwtUtils.java
Original file line number Diff line number Diff line change
@@ -1,49 +1,68 @@
package org.immregistries.iis.kernal;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.MacAlgorithm;
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 javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
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;
}
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;

private final static MacAlgorithm SIGNATURE_ALGORITHM = Jwts.SIG.HS512;
private final static String SIGNATURE_ALGORITHM_NAME = "HmacSha512";

private SecretKey key;

public String generateJwtToken(Authentication authentication) {
init();
return Jwts.builder().id(authentication.getName())
.subject(authentication.getName())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(key, SIGNATURE_ALGORITHM)
.compact();
}

public String getUserNameFromJwtToken(String token) {
init();
return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload().getSubject();
}

public boolean validateJwtToken(String authToken) {
init();
try {
Jwts.parser().verifyWith(key).build().parseSignedClaims(authToken);
return true;
} 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());
} catch (JwtException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
}
return false;
}

private void init() {
if (key == null) {
this.key = new SecretKeySpec(DatatypeConverter.parseBase64Binary(jwtSecret), SIGNATURE_ALGORITHM_NAME);
}
}
}
Loading

0 comments on commit 9e95f46

Please sign in to comment.