Skip to content

Commit

Permalink
P4ADEV-443 userInfo businessLogic
Browse files Browse the repository at this point in the history
  • Loading branch information
antonio.torre committed Jun 5, 2024
1 parent 816d379 commit 51f3f32
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 52 deletions.
21 changes: 21 additions & 0 deletions openapi/p4pa-auth.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,28 @@ components:
issuer:
type: string
organization:
$ref: components/schemas/UserOrganizationRoles
UserOrganizationRoles:
type: object
required:
- id
- name
- fiscalCode
- ipaCode
- roles
properties:
id:
type: string
name:
type: string
fiscalCode:
type: string
ipaCode:
type: string
roles:
type: array
items:
type: string
AuthErrorDTO:
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ public class InvalidTokenException extends RuntimeException {
public InvalidTokenException(String message) {
super(message);
}
public InvalidTokenException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package it.gov.pagopa.payhub.auth.service;

import java.util.Map;
import it.gov.pagopa.payhub.model.generated.UserInfo;

public interface TokenStoreService {
Map<String, String> save(String accessToken, Map<String, String> idTokenClaims);
Map<String, String> load(String accessToken);
UserInfo save(String accessToken, UserInfo userInfo);
UserInfo load(String accessToken);
void delete(String accessToken);
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
package it.gov.pagopa.payhub.auth.service;

import it.gov.pagopa.payhub.auth.configuration.RedisConfig;
import it.gov.pagopa.payhub.model.generated.UserInfo;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
@CacheConfig(cacheNames = RedisConfig.CACHE_NAME_ACCESS_TOKEN)
class TokenStoreServiceImpl implements TokenStoreService{
@Override
@CachePut
public Map<String, String> save(String accessToken, Map<String, String> idTokenClaims) {
public UserInfo save(String accessToken, UserInfo idTokenClaims) {
return idTokenClaims;
}

@Override
@Cacheable
public Map<String, String> load(String accessToken) {
public UserInfo load(String accessToken) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package it.gov.pagopa.payhub.auth.service.exchange;

import com.auth0.jwt.interfaces.Claim;
import it.gov.pagopa.payhub.auth.service.TokenStoreService;
import it.gov.pagopa.payhub.model.generated.AccessToken;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -14,20 +15,22 @@ public class ExchangeTokenServiceImpl implements ExchangeTokenService{
private final ValidateExternalTokenService validateExternalTokenService;
private final AccessTokenBuilderService accessTokenBuilderService;
private final TokenStoreService tokenStoreService;
private final IDTokenClaims2UserInfoMapper idTokenClaimsMapper;

public ExchangeTokenServiceImpl(ValidateExternalTokenService validateExternalTokenService, AccessTokenBuilderService accessTokenBuilderService, TokenStoreService tokenStoreService) {
public ExchangeTokenServiceImpl(ValidateExternalTokenService validateExternalTokenService, AccessTokenBuilderService accessTokenBuilderService, TokenStoreService tokenStoreService, IDTokenClaims2UserInfoMapper idTokenClaimsMapper) {
this.validateExternalTokenService = validateExternalTokenService;
this.accessTokenBuilderService = accessTokenBuilderService;
this.tokenStoreService = tokenStoreService;
this.idTokenClaimsMapper = idTokenClaimsMapper;
}

@Override
public AccessToken postToken(String clientId, String grantType, String subjectToken, String subjectIssuer, String subjectTokenType, String scope) {
log.info("Client {} requested to exchange a {} token provided by {} asking for grant type {} and scope {}",
clientId, subjectTokenType, subjectIssuer, grantType, scope);
Map<String, String> claims = validateExternalTokenService.validate(clientId, grantType, subjectToken, subjectIssuer, subjectTokenType, scope);
Map<String, Claim> claims = validateExternalTokenService.validate(clientId, grantType, subjectToken, subjectIssuer, subjectTokenType, scope);
AccessToken accessToken = accessTokenBuilderService.build();
tokenStoreService.save(accessToken.getAccessToken(), claims);
tokenStoreService.save(accessToken.getAccessToken(), idTokenClaimsMapper.apply(claims));
return accessToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package it.gov.pagopa.payhub.auth.service.exchange;

import com.auth0.jwt.interfaces.Claim;
import io.jsonwebtoken.Claims;
import it.gov.pagopa.payhub.auth.exception.custom.InvalidTokenException;
import it.gov.pagopa.payhub.model.generated.UserInfo;
import it.gov.pagopa.payhub.model.generated.UserOrganizationRoles;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

@Service
public class IDTokenClaims2UserInfoMapper implements Function<Map<String, Claim>, UserInfo> {
@Override
public UserInfo apply(Map<String, Claim> claims) {
try {
return UserInfo.builder()
.issuer(claims.get(Claims.ISSUER).asString())
.userId(claims.get("uid").asString())
.name(claims.get("name").asString())
.familyName(claims.get("family_name").asString())
.fiscalCode(claims.get("fiscal_number").asString())
.organization(buildUserOrganizationRoles(claims))
.build();
} catch (Exception e){
throw new InvalidTokenException("Unexpected IDToken structure", e);
}
}

private UserOrganizationRoles buildUserOrganizationRoles(Map<String, Claim> claims) {
Map<String, Object> organizationClaim = claims.get("organization").asMap();

List<String> roles = readUserOrganizationRoles(organizationClaim);
return UserOrganizationRoles.builder()
.id((String)organizationClaim.get("id"))
.name((String)organizationClaim.get("name"))
.fiscalCode((String)organizationClaim.get("fiscal_code"))
.ipaCode((String)organizationClaim.get("ipaCode"))
.roles(roles)
.build();
}

private static List<String> readUserOrganizationRoles(Map<String, Object> organizationClaim) {
List<String> out;
if(organizationClaim.get("roles") instanceof List<?> roles){
out = roles.stream().map(o -> (o instanceof Map<?, ?> orgMap) ? (String) orgMap.get("role") : null)
.filter(Objects::nonNull)
.toList();
} else {
out = List.of();
}
if(out.isEmpty()){
throw new InvalidTokenException("No organization roles provided");
} else {
return out;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package it.gov.pagopa.payhub.auth.service.exchange;

import com.auth0.jwt.interfaces.Claim;
import io.jsonwebtoken.Claims;
import it.gov.pagopa.payhub.auth.exception.custom.*;
import it.gov.pagopa.payhub.auth.utils.JWTValidator;
Expand All @@ -11,7 +12,7 @@

@Service
@Slf4j
class ValidateExternalTokenService {
public class ValidateExternalTokenService {
public static final String ALLOWED_CLIENT_ID = "piattaforma-unitaria";
public static final String ALLOWED_GRANT_TYPE="urn:ietf:params:oauth:grant-type:token-exchange";
public static final String ALLOWED_SUBJECT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt";
Expand All @@ -33,11 +34,11 @@ public ValidateExternalTokenService(@Value("${jwt.audience:}")String allowedAudi
this.jwtValidator = jwtValidator;
}

public Map<String, String> validate(String clientId, String grantType, String subjectToken, String subjectIssuer, String subjectTokenType, String scope) {
public Map<String, Claim> validate(String clientId, String grantType, String subjectToken, String subjectIssuer, String subjectTokenType, String scope) {
validateClient(clientId);
validateProtocolConfiguration(grantType, subjectTokenType, scope);
validateSubjectTokenIssuer(subjectIssuer);
Map<String, String> claims = validateSubjectToken(subjectToken);
Map<String, Claim> claims = validateSubjectToken(subjectToken);
log.info("SubjectToken authorized");
return claims;
}
Expand Down Expand Up @@ -66,12 +67,12 @@ private void validateSubjectTokenIssuer(String subjectIssuer) {
}
}

private Map<String, String> validateSubjectToken(String subjectToken) {
Map<String, String> claims = jwtValidator.validate(subjectToken, urlJwkProvider);
if (!allowedAudience.equals(claims.get(Claims.AUDIENCE))){
private Map<String, Claim> validateSubjectToken(String subjectToken) {
Map<String, Claim> claims = jwtValidator.validate(subjectToken, urlJwkProvider);
if (!allowedAudience.equals(claims.get(Claims.AUDIENCE).asString())){
throw new InvalidTokenException("Invalid audience: " + allowedAudience);
}
if (!allowedIssuer.equals(claims.get(Claims.ISSUER))){
if (!allowedIssuer.equals(claims.get(Claims.ISSUER).asString())){
throw new InvalidTokenException("Invalid issuer: " + allowedIssuer);
}
return claims;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package it.gov.pagopa.payhub.auth.service.user;

import it.gov.pagopa.payhub.auth.exception.custom.InvalidAccessTokenException;
import it.gov.pagopa.payhub.auth.service.TokenStoreService;
import it.gov.pagopa.payhub.model.generated.UserInfo;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService{

private final TokenStoreService tokenStoreService;
public UserServiceImpl(TokenStoreService tokenStoreService) {
this.tokenStoreService = tokenStoreService;
}

@Override
public UserInfo getUserInfo(String accessToken) {
return null; //TODO
UserInfo userInfo = tokenStoreService.load(accessToken);
if(userInfo==null){
throw new InvalidAccessTokenException("AccessToken not found");
} else {
return userInfo;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import it.gov.pagopa.payhub.auth.exception.custom.InvalidTokenException;
import it.gov.pagopa.payhub.auth.exception.custom.TokenExpiredException;
import org.springframework.stereotype.Component;

import java.security.interfaces.RSAPublicKey;
import java.util.HashMap;
import java.util.Map;


Expand All @@ -35,7 +35,7 @@ public class JWTValidator {
* @throws InvalidTokenException if the token is invalid for any other reason
*/

public Map<String, String> validate(String token, String urlJwkProvider) {
public Map<String, Claim> validate(String token, String urlJwkProvider) {
try {
DecodedJWT jwt = JWT.decode(token);

Expand All @@ -45,10 +45,7 @@ public Map<String, String> validate(String token, String urlJwkProvider) {
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);

Map<String, String> claimsMap = new HashMap<>();
jwt.getClaims().forEach((key, value) -> claimsMap.put(key, value.asString()));

return claimsMap;
return jwt.getClaims();

} catch (com.auth0.jwt.exceptions.TokenExpiredException e){
throw new TokenExpiredException(e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
package it.gov.pagopa.payhub.auth.service;

import it.gov.pagopa.payhub.model.generated.UserInfo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;

class TokenStoreServiceTest {

private final TokenStoreService service = new TokenStoreServiceImpl();

@Test
void givenClaimsWhenSaveThenReturnThem(){
// Given
HashMap<String, String> idTokenClaims = new HashMap<>();
UserInfo userInfo = new UserInfo();
String accessToken = "AccessToken";

// When
Map<String, String> result = service.save(accessToken, idTokenClaims);
UserInfo result = service.save(accessToken, userInfo);

// Then
Assertions.assertSame(idTokenClaims, result);
Assertions.assertSame(userInfo, result);
}

@Test
Expand All @@ -29,7 +27,7 @@ void givenAccessTokenWhenSaveThenNull(){
String accessToken = "AccessToken";

// When
Map<String, String> result = service.load(accessToken);
UserInfo result = service.load(accessToken);

// Then
Assertions.assertNull(result);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package it.gov.pagopa.payhub.auth.service.exchange;

import com.auth0.jwt.interfaces.Claim;
import it.gov.pagopa.payhub.auth.service.TokenStoreService;
import it.gov.pagopa.payhub.model.generated.AccessToken;
import it.gov.pagopa.payhub.model.generated.UserInfo;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -22,20 +24,23 @@ class ExchangeTokenServiceTest {
private AccessTokenBuilderService accessTokenBuilderServiceMock;
@Mock
private TokenStoreService tokenStoreServiceMock;
@Mock
private IDTokenClaims2UserInfoMapper idTokenClaimsMapperMock;

private ExchangeTokenService service;

@BeforeEach
void init(){
service = new ExchangeTokenServiceImpl(validateExternalTokenServiceMock, accessTokenBuilderServiceMock, tokenStoreServiceMock);
service = new ExchangeTokenServiceImpl(validateExternalTokenServiceMock, accessTokenBuilderServiceMock, tokenStoreServiceMock, idTokenClaimsMapperMock);
}

@AfterEach
void verifyNotMoreInteractions(){
Mockito.verifyNoMoreInteractions(
validateExternalTokenServiceMock,
accessTokenBuilderServiceMock,
tokenStoreServiceMock
tokenStoreServiceMock,
idTokenClaimsMapperMock
);
}

Expand All @@ -49,19 +54,23 @@ void givenValidTokenWhenPostTokenThenSuccess(){
String subjectTokenType="SUBJECT_TOKEN_TYPE";
String scope="SCOPE";

HashMap<String, String> expectedClaims = new HashMap<>();
HashMap<String, Claim> expectedClaims = new HashMap<>();
Mockito.when(validateExternalTokenServiceMock.validate(clientId, grantType, subjectToken, subjectIssuer, subjectTokenType, scope))
.thenReturn(expectedClaims);

AccessToken expectedAccessToken = AccessToken.builder().accessToken("accessToken").build();
Mockito.when(accessTokenBuilderServiceMock.build())
.thenReturn(expectedAccessToken);

UserInfo userInfo = new UserInfo();
Mockito.when(idTokenClaimsMapperMock.apply(expectedClaims))
.thenReturn(userInfo);

// When
AccessToken result = service.postToken(clientId, grantType, subjectToken, subjectIssuer, subjectTokenType, scope);

// Then
Assertions.assertSame(expectedAccessToken, result);
Mockito.verify(tokenStoreServiceMock).save(Mockito.same(expectedAccessToken.getAccessToken()), Mockito.same(expectedClaims));
Mockito.verify(tokenStoreServiceMock).save(Mockito.same(expectedAccessToken.getAccessToken()), Mockito.same(userInfo));
}
}
Loading

0 comments on commit 51f3f32

Please sign in to comment.