Skip to content

Commit

Permalink
P4ADEV-788 added BL
Browse files Browse the repository at this point in the history
  • Loading branch information
macacia committed Nov 5, 2024
1 parent 580a6b0 commit daf6f62
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package it.gov.pagopa.payhub.auth.mapper;

import it.gov.pagopa.payhub.auth.dto.IamUserInfoDTO;
import it.gov.pagopa.payhub.auth.dto.IamUserOrganizationRolesDTO;
import it.gov.pagopa.payhub.auth.utils.Constants;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.UUID;

@Service
public class A2ALegacyClaims2UserInfoMapper {

public IamUserInfoDTO map(String subject) {
return IamUserInfoDTO.builder()
.systemUser(true)
.issuer(subject)
.userId(UUID.randomUUID().toString())
.name(subject)
.familyName(subject)
.fiscalCode(subject)
.organizationAccess(IamUserOrganizationRolesDTO.builder()
.organizationIpaCode(subject)
.roles(Collections.singletonList(Constants.ROLE_ADMIN))
.build())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import it.gov.pagopa.payhub.auth.exception.custom.InvalidAccessTokenException;
import it.gov.pagopa.payhub.auth.service.AuthnService;
import it.gov.pagopa.payhub.auth.service.ValidateTokenService;
import it.gov.pagopa.payhub.auth.service.a2a.legacy.JWTLegacyHandlerService;
import it.gov.pagopa.payhub.model.generated.UserInfo;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -28,10 +29,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final AuthnService authnService;
private final ValidateTokenService validateTokenService;
private final JWTLegacyHandlerService jwtLegacyHandlerService;

public JwtAuthenticationFilter(AuthnService authnService, ValidateTokenService validateTokenService) {
this.authnService = authnService;
this.validateTokenService = validateTokenService;
public JwtAuthenticationFilter(AuthnService authnService, ValidateTokenService validateTokenService, JWTLegacyHandlerService jwtLegacyHandlerService) {
this.authnService = authnService;
this.validateTokenService = validateTokenService;
this.jwtLegacyHandlerService = jwtLegacyHandlerService;
}

@Override
Expand All @@ -40,7 +43,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(authorization)) {
String token = authorization.replace("Bearer ", "");
validateTokenService.validate(token);
if (validateTokenService.isInternalToken(token)) {
validateTokenService.validate(token);
} else {
jwtLegacyHandlerService.handleLegacyToken(token);
}
UserInfo userInfo = authnService.getUserInfo(token);
Collection<? extends GrantedAuthority> authorities = null;
if (userInfo.getOrganizationAccess() != null) {
Expand All @@ -62,5 +69,4 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

filterChain.doFilter(request, response);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package it.gov.pagopa.payhub.auth.service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.RegisteredClaims;
import com.auth0.jwt.algorithms.Algorithm;
import it.gov.pagopa.payhub.auth.utils.CertUtils;
import it.gov.pagopa.payhub.model.generated.AccessToken;
Expand All @@ -19,6 +20,7 @@

@Service
public class AccessTokenBuilderService {
public static final String ISSUER = "p4pa-auth";
public static final String ACCESS_TOKEN_TYPE = "at+JWT";
private final String allowedAudience;
private final int expireIn;
Expand Down Expand Up @@ -46,6 +48,7 @@ public AccessTokenBuilderService(
public AccessToken build(){
Algorithm algorithm = Algorithm.RSA512(rsaPublicKey, rsaPrivateKey);
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put(RegisteredClaims.ISSUER, ISSUER);
headerClaims.put("typ", ACCESS_TOKEN_TYPE);
String tokenType = "bearer";
String token = JWT.create()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package it.gov.pagopa.payhub.auth.service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.RegisteredClaims;
import com.auth0.jwt.interfaces.DecodedJWT;
import it.gov.pagopa.payhub.auth.exception.custom.InvalidTokenException;
import it.gov.pagopa.payhub.auth.utils.JWTValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.Base64;

@Service
@Slf4j
public class ValidateTokenService {
Expand All @@ -27,4 +30,10 @@ private void validateAccessType(String type) {
throw new InvalidTokenException("Invalid token type " + type);
}
}

public boolean isInternalToken(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getHeaderClaim(RegisteredClaims.ISSUER).asString().equalsIgnoreCase(AccessTokenBuilderService.ISSUER);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package it.gov.pagopa.payhub.auth.service.a2a.legacy;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Map;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "m2m.legacy.public")
public class A2AClientLegacyPropConfig {
private Map<String, String> secrets;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package it.gov.pagopa.payhub.auth.service.a2a.legacy;

import io.jsonwebtoken.io.Decoders;
import it.gov.pagopa.payhub.auth.exception.custom.InvalidTokenException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class A2ALegacySecretsRetrieverService {
private final A2AClientLegacyPropConfig a2AClientLegacyPropConfig;

public A2ALegacySecretsRetrieverService(A2AClientLegacyPropConfig a2AClientLegacyPropConfig) {
this.a2AClientLegacyPropConfig = a2AClientLegacyPropConfig;
}

public Map<String, PublicKey> envToMapByPrefix(String prefix) {
Map<String, PublicKey> secretMap = new HashMap<>();
a2AClientLegacyPropConfig.getSecrets()
.forEach((key, value) ->
secretMap.put(key, getPublicKeyFromString(value))
);
return secretMap;
}

private PublicKey getPublicKeyFromString(String encodedKey) {
try {
X509EncodedKeySpec publicKeyX509 = new X509EncodedKeySpec(Decoders.BASE64.decode(encodedKey));
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(publicKeyX509);
} catch (Exception e){
throw new InvalidTokenException("invalid public key");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package it.gov.pagopa.payhub.auth.service.a2a.legacy;

import com.auth0.jwt.RegisteredClaims;
import com.auth0.jwt.interfaces.Claim;
import it.gov.pagopa.payhub.auth.dto.IamUserInfoDTO;
import it.gov.pagopa.payhub.auth.mapper.A2ALegacyClaims2UserInfoMapper;
import it.gov.pagopa.payhub.auth.service.TokenStoreService;
import it.gov.pagopa.payhub.model.generated.AccessToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
@Slf4j
public class JWTLegacyHandlerService {
private final ValidateJWTLegacyService validateJWTLegacyService;
private final TokenStoreService tokenStoreService;
private final A2ALegacyClaims2UserInfoMapper a2ALegacyClaims2UserInfoMapper;

public JWTLegacyHandlerService(ValidateJWTLegacyService validateJWTLegacyService, TokenStoreService tokenStoreService, A2ALegacyClaims2UserInfoMapper a2ALegacyClaims2UserInfoMapper) {
this.validateJWTLegacyService = validateJWTLegacyService;
this.tokenStoreService = tokenStoreService;
this.a2ALegacyClaims2UserInfoMapper = a2ALegacyClaims2UserInfoMapper;
}

public void handleLegacyToken(String token) {
Pair<String, Map<String, Claim>> claims = validateJWTLegacyService.validate(token);
AccessToken accessToken = AccessToken.builder()
.accessToken(token)
.tokenType("bearer")
.expiresIn(claims.getRight().get(RegisteredClaims.EXPIRES_AT).asInt())
.build();
IamUserInfoDTO iamUser = a2ALegacyClaims2UserInfoMapper.map(claims.getLeft());
tokenStoreService.save(accessToken.getAccessToken(), iamUser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package it.gov.pagopa.payhub.auth.service.a2a.legacy;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.RegisteredClaims;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import it.gov.pagopa.payhub.auth.exception.custom.InvalidTokenException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Service;

import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Map;

@Service
public class ValidateJWTLegacyService {
private static final String ENV_M2M_LEGACY_SECRET_PREFIX = "m2m.legacy.public";
private static final String TOKEN_TYPE_A2A = "a2a";

private final A2ALegacySecretsRetrieverService a2ALegacySecretsRetrieverService;
private final Map<String, PublicKey> clientApplicationsPublicKey;

public ValidateJWTLegacyService(A2ALegacySecretsRetrieverService a2ALegacySecretsRetrieverService) {
this.a2ALegacySecretsRetrieverService = a2ALegacySecretsRetrieverService;
this.clientApplicationsPublicKey = a2ALegacySecretsRetrieverService.envToMapByPrefix(ENV_M2M_LEGACY_SECRET_PREFIX);
}

public Pair<String, Map<String, Claim>> validate(String token) {
verifyEnvMap();
Pair<String, Map<String, Claim>> claims = this.clientApplicationsPublicKey.keySet().stream()
.map(key -> new ImmutablePair<>(key, this.clientApplicationsPublicKey.get(key)))
.map(pair -> validateLegacyToken(pair.getLeft(), token, pair.getRight()))
.findFirst()
.orElseThrow(() -> new InvalidTokenException("Invalid token for A2A call"));
isA2AAuthToken(claims.getRight());
validateClaims(claims.getRight());
validateSubject(claims.getLeft());
return claims;
}

private void verifyEnvMap() {
if (this.clientApplicationsPublicKey.isEmpty()){
throw new InvalidTokenException("The token is not present");
}
}

public void isA2AAuthToken(Map<String, Claim> claims){
if (TOKEN_TYPE_A2A.equals(claims.getOrDefault("type", null)))
throw new InvalidTokenException("Invalid token type");
}

private void validateClaims(Map<String, Claim> claims) {
if (claims.get(RegisteredClaims.ISSUED_AT).asInstant().isBefore(Instant.now().minusSeconds(3600 * 24))) {
throw new InvalidTokenException("Invalid field iat");
}
if (claims.get(RegisteredClaims.EXPIRES_AT).asInstant().isAfter(Instant.now().plusSeconds(3600 * 24))) {
throw new InvalidTokenException("Invalid field exp");
}
if (StringUtils.isBlank(claims.get(RegisteredClaims.JWT_ID).asString())) {
throw new InvalidTokenException("Invalid field jti");
}
}

private void validateSubject(String subject) {
if (StringUtils.isBlank(subject)) {
throw new InvalidTokenException("Invalid subject");
}
}

private Pair<String, Map<String, Claim>> validateLegacyToken(String app, String token, PublicKey publicKey) {
try{
DecodedJWT jwt = JWT.decode(token);

Algorithm algorithm = Algorithm.RSA512((RSAPublicKey) publicKey);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwt);

return new ImmutablePair<>(app, jwt.getClaims());
} catch (Exception e) {
return null;
}
}
}
2 changes: 2 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,5 @@ data-chiper:

m2m:
piattaforma-unitaria-client-secret: "\${PIATTAFORMA_UNITARIA_CLIENT_SECRET:SECRET}"
# list here the keys used to identify external apps
# for example if an app is called "acme" then you should define its public key in the property "m2m.legacy.public.acme"
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package it.gov.pagopa.payhub.auth.service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.RegisteredClaims;
import com.auth0.jwt.interfaces.DecodedJWT;
import it.gov.pagopa.payhub.model.generated.AccessToken;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -76,7 +77,7 @@ void test(){
String decodedHeader = new String(Base64.getDecoder().decode(decodedAccessToken.getHeader()));
String decodedPayload = new String(Base64.getDecoder().decode(decodedAccessToken.getPayload()));

Assertions.assertEquals("{\"typ\":\"at+JWT\",\"alg\":\"RS512\"}", decodedHeader);
Assertions.assertEquals("{\"iss\":\"p4pa-auth\",\"typ\":\"at+JWT\",\"alg\":\"RS512\"}", decodedHeader);
Assertions.assertEquals(EXPIRE_IN, (decodedAccessToken.getExpiresAtAsInstant().toEpochMilli() - decodedAccessToken.getIssuedAtAsInstant().toEpochMilli()) / 1_000);
Assertions.assertTrue(Pattern.compile("\\{\"typ\":\"bearer\",\"iss\":\"APPLICATION_AUDIENCE\",\"jti\":\"[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}\",\"iat\":[0-9]+,\"exp\":[0-9]+}").matcher(decodedPayload).matches(), "Payload not matches requested pattern: " + decodedPayload);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package it.gov.pagopa.payhub.auth.service.a2a.retrieve;

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import it.gov.pagopa.payhub.auth.service.a2a.legacy.A2AClientLegacyPropConfig;
import it.gov.pagopa.payhub.auth.service.a2a.legacy.A2ALegacySecretsRetrieverService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class A2ALegacySecretsRetrieverServiceTest {

@Mock
private A2AClientLegacyPropConfig a2aClientLegacyPropConfigMock;

private A2ALegacySecretsRetrieverService service;

private KeyPair keyPair;

@BeforeEach
public void setUp() {
service = new A2ALegacySecretsRetrieverService(a2aClientLegacyPropConfigMock);
keyPair = Keys.keyPairFor(SignatureAlgorithm.RS512);
}

@Test
public void givenEnvPropWhenEnvToMapByPrefixThenGetKey() {
//Given
String secretKeyPrefix = "m2m.legacy.public";
PublicKey publicKey = keyPair.getPublic();
String publicKeyEncoded = Encoders.BASE64.encode(keyPair.getPublic().getEncoded());
Map<String, String> mockSettings = Map.of("acme", publicKeyEncoded);
when(a2aClientLegacyPropConfigMock.getSecrets()).thenReturn(mockSettings);
//When

Map<String, PublicKey> result = service.envToMapByPrefix(secretKeyPrefix);

//Then
assertNotNull(result);
assertEquals(publicKey, result.get("acme"));
}

@Test
public void WhenEnvToMapByPrefixThenGetEmptyMap() {
//When
Map<String, PublicKey> result = service.envToMapByPrefix("nonExistent");
//Then
assertEquals(0, result.size());
}
}

0 comments on commit daf6f62

Please sign in to comment.