Skip to content

Commit

Permalink
P4ADEV-394 validate postToken request params
Browse files Browse the repository at this point in the history
  • Loading branch information
antonio.torre committed May 29, 2024
1 parent 05270f0 commit f0a98ee
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package it.gov.pagopa.payhub.auth.exception;

import it.gov.pagopa.payhub.auth.exception.custom.InvalidTokenException;
import it.gov.pagopa.payhub.auth.exception.custom.TokenExpiredException;
import it.gov.pagopa.payhub.auth.exception.custom.*;
import it.gov.pagopa.payhub.model.generated.AuthErrorDTO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -19,20 +18,36 @@
@Order(Ordered.HIGHEST_PRECEDENCE)
public class AuthExceptionHandler {

@ExceptionHandler(InvalidTokenException.class)
@ExceptionHandler({InvalidTokenException.class, TokenExpiredException.class})
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public AuthErrorDTO handleInvalidTokenException(InvalidTokenException ex, HttpServletRequest request){
public AuthErrorDTO handleInvalidGrantError(RuntimeException ex, HttpServletRequest request){
String message = logAndReturnUnauthorizedExceptionMessage(ex, request);

return new AuthErrorDTO(AuthErrorDTO.ErrorEnum.INVALID_GRANT, message);
}

@ExceptionHandler(TokenExpiredException.class)
@ExceptionHandler(InvalidExchangeClientException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public AuthErrorDTO handleTokenExpiredException(TokenExpiredException ex, HttpServletRequest request){
public AuthErrorDTO handleInvalidClientError(RuntimeException ex, HttpServletRequest request){
String message = logAndReturnUnauthorizedExceptionMessage(ex, request);

return new AuthErrorDTO(AuthErrorDTO.ErrorEnum.INVALID_GRANT, message);
return new AuthErrorDTO(AuthErrorDTO.ErrorEnum.INVALID_CLIENT, message);
}

@ExceptionHandler({InvalidExchangeRequestException.class, InvalidTokenIssuerException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public AuthErrorDTO handleInvalidRequestError(RuntimeException ex, HttpServletRequest request){
String message = logAndReturnUnauthorizedExceptionMessage(ex, request);

return new AuthErrorDTO(AuthErrorDTO.ErrorEnum.INVALID_REQUEST, message);
}

@ExceptionHandler({InvalidGrantTypeException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public AuthErrorDTO handleUnsupportedGrantType(RuntimeException ex, HttpServletRequest request){
String message = logAndReturnUnauthorizedExceptionMessage(ex, request);

return new AuthErrorDTO(AuthErrorDTO.ErrorEnum.UNSUPPORTED_GRANT_TYPE, message);
}

@ExceptionHandler(MissingServletRequestParameterException.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package it.gov.pagopa.payhub.auth.exception.custom;

import lombok.Getter;

@Getter
public class InvalidExchangeClientException extends RuntimeException {

public InvalidExchangeClientException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package it.gov.pagopa.payhub.auth.exception.custom;

import lombok.Getter;

@Getter
public class InvalidExchangeRequestException extends RuntimeException {

public InvalidExchangeRequestException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package it.gov.pagopa.payhub.auth.exception.custom;

import lombok.Getter;

@Getter
public class InvalidGrantTypeException extends RuntimeException {

public InvalidGrantTypeException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package it.gov.pagopa.payhub.auth.exception.custom;

import lombok.Getter;

@Getter
public class InvalidTokenIssuerException extends RuntimeException {

public InvalidTokenIssuerException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package it.gov.pagopa.payhub.auth.service.exchange;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class ExchangeTokenServiceImpl implements ExchangeTokenService{

private final ValidateTokenService validateTokenService;
Expand All @@ -13,6 +15,8 @@ public ExchangeTokenServiceImpl(ValidateTokenService validateTokenService) {

@Override
public void 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);
validateTokenService.validate(clientId, grantType, subjectToken, subjectIssuer, subjectTokenType, scope);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package it.gov.pagopa.payhub.auth.service.exchange;

import io.jsonwebtoken.Claims;
import it.gov.pagopa.payhub.auth.exception.custom.InvalidTokenException;
import it.gov.pagopa.payhub.auth.exception.custom.*;
import it.gov.pagopa.payhub.auth.utils.JWTValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -12,26 +12,65 @@
@Service
@Slf4j
class ValidateTokenService {
private final String audience;
private final String issuer;
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:id_token";
public static final String ALLOWED_SCOPE="openid";

private final String allowedAudience;
private final String allowedIssuer;
private final String urlJwkProvider;
private final JWTValidator jwtValidator;

public ValidateTokenService(@Value("${jwt.token.audience:}")String audience,
@Value("${jwt.token.issuer:}")String issuer,
public ValidateTokenService(@Value("${jwt.token.audience:}")String allowedAudience,
@Value("${jwt.token.issuer:}")String allowedIssuer,
@Value("${jwt.token.jwk:}")String urlJwkProvider,
JWTValidator jwtValidator) {
this.audience = audience;
this.issuer = issuer;
this.allowedAudience = allowedAudience;
this.allowedIssuer = allowedIssuer;

this.urlJwkProvider = urlJwkProvider;
this.jwtValidator = jwtValidator;
}

public void validate(String clientId, String grantType, String subjectToken, String subjectIssuer, String subjectTokenType, String scope) {
validateClient(clientId);
validateProtocolConfiguration(grantType, subjectTokenType, scope);
validateSubjectTokenIssuer(subjectIssuer);
validateSubjectToken(subjectToken);
}

private void validateClient(String clientId) {
if (!ALLOWED_CLIENT_ID.equals(clientId)){
throw new InvalidExchangeClientException("Invalid clientId " + clientId);
}
}

private void validateProtocolConfiguration(String grantType, String subjectTokenType, String scope) {
if (!ALLOWED_GRANT_TYPE.equals(grantType)){
throw new InvalidGrantTypeException("Invalid grantType " + grantType);
}
if (!ALLOWED_SUBJECT_TOKEN_TYPE.equals(subjectTokenType)){
throw new InvalidTokenException("Invalid subjectTokenType " + subjectTokenType);
}
if (!ALLOWED_SCOPE.equals(scope)){
throw new InvalidExchangeRequestException("Invalid scope " + scope);
}
}

private void validateSubjectTokenIssuer(String subjectIssuer) {
if (!allowedIssuer.equals(subjectIssuer)){
throw new InvalidTokenIssuerException("Invalid subjectIssuer " + subjectIssuer);
}
}

private void validateSubjectToken(String subjectToken) {
Map<String, String> data = jwtValidator.validate(subjectToken, urlJwkProvider);
if (!(data.get(Claims.AUDIENCE).equals(audience) && data.get(Claims.ISSUER).equals(issuer))){
throw new InvalidTokenException("Invalid audience or issuer in the token");
if (!allowedAudience.equals(data.get(Claims.AUDIENCE))){
throw new InvalidTokenException("Invalid audience: " + allowedAudience);
}
if (!allowedIssuer.equals(data.get(Claims.ISSUER))){
throw new InvalidTokenException("Invalid issuer: " + allowedIssuer);
}
log.info("Token validated successfully");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import it.gov.pagopa.payhub.auth.exception.AuthExceptionHandler;
import it.gov.pagopa.payhub.auth.exception.custom.*;
import it.gov.pagopa.payhub.auth.service.AuthService;
import it.gov.pagopa.payhub.model.generated.AuthErrorDTO;
import org.junit.jupiter.api.Assertions;
Expand All @@ -10,11 +11,13 @@
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

Expand All @@ -28,18 +31,69 @@ class AuthControllerTest {
private ObjectMapper objectMapper;

@MockBean
AuthService authServiceMock;
private AuthService authServiceMock;

@Test
void givenExpectedAuthTokenThenOk() throws Exception {
MvcResult result =
invokeAndVerify(null, HttpStatus.OK, null);

Assertions.assertNotNull(result);
}

@Test
void givenRequestWithoutAuthTokenThenBadRequest() throws Exception {
MvcResult result = mockMvc.perform(
post("/payhub/auth/token")
).andExpect(status().isBadRequest()).andReturn();

AuthErrorDTO actual = objectMapper.readValue(result.getResponse().getContentAsString(),
AuthErrorDTO.class);
assertEquals(AuthErrorDTO.ErrorEnum.INVALID_REQUEST, actual.getError());
}

@Test
void givenInvalidExchangeClientExceptionThenInvalidClientError() throws Exception {
invokeAndVerify(new InvalidExchangeClientException("description"), HttpStatus.UNAUTHORIZED, AuthErrorDTO.ErrorEnum.INVALID_CLIENT);
}

@Test
void givenInvalidExchangeRequestExceptionThenInvalidClientError() throws Exception {
invokeAndVerify(new InvalidExchangeRequestException("description"), HttpStatus.BAD_REQUEST, AuthErrorDTO.ErrorEnum.INVALID_REQUEST);
}

@Test
void givenInvalidGrantTypeExceptionThenInvalidClientError() throws Exception {
invokeAndVerify(new InvalidGrantTypeException("description"), HttpStatus.BAD_REQUEST, AuthErrorDTO.ErrorEnum.UNSUPPORTED_GRANT_TYPE);
}

@Test
void givenInvalidTokenExceptionThenInvalidClientError() throws Exception {
invokeAndVerify(new InvalidTokenException("description"), HttpStatus.UNAUTHORIZED, AuthErrorDTO.ErrorEnum.INVALID_GRANT);
}

@Test
void givenInvalidTokenIssuerExceptionThenInvalidClientError() throws Exception {
invokeAndVerify(new InvalidTokenIssuerException("description"), HttpStatus.BAD_REQUEST, AuthErrorDTO.ErrorEnum.INVALID_REQUEST);
}

@Test
void givenTokenExpiredExceptionThenInvalidClientError() throws Exception {
invokeAndVerify(new TokenExpiredException("description"), HttpStatus.UNAUTHORIZED, AuthErrorDTO.ErrorEnum.INVALID_GRANT);
}

MvcResult invokeAndVerify(RuntimeException exception, HttpStatus expectedStatus, AuthErrorDTO.ErrorEnum expectedError) throws Exception {
String clientId="CLIENT_ID";
String grantType="GRANT_TYPE";
String subjectToken="SUBJECT_TOKEN";
String subjectIssuer="SUBJECT_ISSUER";
String subjectTokenType="SUBJECT_TOKEN_TYPE";
String scope="SCOPE";

doNothing().when(authServiceMock).postToken(clientId,grantType,subjectToken,subjectIssuer,subjectTokenType,scope);
(exception != null
? doThrow(exception)
: doNothing())
.when(authServiceMock).postToken(clientId,grantType,subjectToken,subjectIssuer,subjectTokenType,scope);

MvcResult result = mockMvc.perform(
post("/payhub/auth/token")
Expand All @@ -49,19 +103,17 @@ void givenExpectedAuthTokenThenOk() throws Exception {
.param("subject_issuer", subjectIssuer)
.param("subject_token_type", subjectTokenType)
.param("scope", scope)
).andExpect(status().is2xxSuccessful()).andReturn();
).andExpect(status().is(expectedStatus.value())).andReturn();

Assertions.assertNotNull(result);
}
if(exception != null && expectedError != null) {
AuthErrorDTO actual = objectMapper.readValue(result.getResponse().getContentAsString(),
AuthErrorDTO.class);
assertEquals(expectedError, actual.getError());
assertEquals(exception.getMessage(), actual.getErrorDescription());
} else {
Assertions.assertFalse(result.getResponse().getContentAsString().contains("error"));
}

@Test
void givenRequestWithoutAuthTokenThenBadRequest() throws Exception {
MvcResult result = mockMvc.perform(
post("/payhub/auth/token")
).andExpect(status().isBadRequest()).andReturn();

AuthErrorDTO actual = objectMapper.readValue(result.getResponse().getContentAsString(),
AuthErrorDTO.class);
assertEquals(AuthErrorDTO.ErrorEnum.INVALID_REQUEST, actual.getError());
return result;
}
}
Loading

0 comments on commit f0a98ee

Please sign in to comment.