diff --git a/kernel/kernel-authcodeflowproxy-api/pom.xml b/kernel/kernel-authcodeflowproxy-api/pom.xml index 5e603c5d65a..3ba3ede2f85 100644 --- a/kernel/kernel-authcodeflowproxy-api/pom.xml +++ b/kernel/kernel-authcodeflowproxy-api/pom.xml @@ -222,6 +222,7 @@ 1.4.2 1.2.1-SNAPSHOT 0.8.5 + 2.0.0 @@ -239,6 +240,18 @@ jwks-rsa 0.18.0 + + org.powermock + powermock-api-mockito2 + ${powermock.version} + test + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + diff --git a/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/controller/LoginController.java b/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/controller/LoginController.java index 449285ad1a3..91cabb78112 100644 --- a/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/controller/LoginController.java +++ b/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/controller/LoginController.java @@ -33,6 +33,8 @@ @RestController public class LoginController { + private static final String ID_TOKEN = "id_token"; + private final static Logger LOGGER= LoggerFactory.getLogger(LoginController.class); @Value("${auth.token.header:Authorization}") @@ -46,7 +48,10 @@ public class LoginController { private LoginService loginService; @Autowired - private ValidateTokenHelper validateTokenHelper; + private ValidateTokenHelper validateTokenHelper; + + @Value("${auth.validate.id-token:false}") + private boolean validateIdToken; @GetMapping(value = "/login/{redirectURI}") public void login(@CookieValue(name = "state", required = false) String state, @@ -88,11 +93,17 @@ public void loginRedirect(@PathVariable("redirectURI") String redirectURI, @Requ redirectURI); String accessToken = jwtResponseDTO.getAccessToken(); validateToken(accessToken); - String idToken = jwtResponseDTO.getIdToken(); - validateToken(idToken); Cookie cookie = loginService.createCookie(accessToken); res.addCookie(cookie); - res.addCookie(new Cookie("id_token", idToken)); + if(validateIdToken) { + String idToken = jwtResponseDTO.getIdToken(); + if(idToken == null) { + throw new ClientException(Errors.TOKEN_NOTPRESENT_ERROR.getErrorCode(), + Errors.TOKEN_NOTPRESENT_ERROR.getErrorMessage() + ": " + ID_TOKEN); + } + validateToken(idToken); + res.addCookie(new Cookie(ID_TOKEN, idToken)); + } res.setStatus(302); String url = new String(Base64.decodeBase64(redirectURI.getBytes())); if(url.contains("#")) { @@ -103,7 +114,7 @@ public void loginRedirect(@PathVariable("redirectURI") String redirectURI, @Requ throw new ServiceException(Errors.ALLOWED_URL_EXCEPTION.getErrorCode(), Errors.ALLOWED_URL_EXCEPTION.getErrorMessage()); } res.sendRedirect(url); - } + } private void validateToken(String accessToken) { if(!validateTokenHelper.isTokenValid(accessToken).getKey()){ diff --git a/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/exception/AuthCodeProxyExceptionHandler.java b/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/exception/AuthCodeProxyExceptionHandler.java index 97a5b85209b..adbf343b7d5 100644 --- a/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/exception/AuthCodeProxyExceptionHandler.java +++ b/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/exception/AuthCodeProxyExceptionHandler.java @@ -46,8 +46,14 @@ public ResponseEntity> clientException( public ResponseEntity> servieException( HttpServletRequest httpServletRequest, final ServiceException e) throws IOException { ExceptionUtils.logRootCause(e); + HttpStatus status; + if(e.getErrorCode().equals(Errors.INVALID_TOKEN.getErrorCode())) { + status = HttpStatus.UNAUTHORIZED; + } else { + status = HttpStatus.OK; + } return new ResponseEntity<>( - getErrorResponse(httpServletRequest, e.getErrorCode(), e.getErrorText()), HttpStatus.OK); + getErrorResponse(httpServletRequest, e.getErrorCode(), e.getErrorText()), status); } @ExceptionHandler(AuthenticationServiceException.class) diff --git a/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/service/validator/ScopeValidator.java b/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/service/validator/ScopeValidator.java deleted file mode 100644 index bae4b134674..00000000000 --- a/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/service/validator/ScopeValidator.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.mosip.kernel.authcodeflowproxy.api.service.validator; - -import java.util.List; -import java.util.function.BiPredicate; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; - -import com.auth0.jwt.JWT; -import com.auth0.jwt.interfaces.DecodedJWT; - -import io.mosip.kernel.core.authmanager.authadapter.model.AuthUserDetails; -/** - * Validator used to validate the scope in the token - * - * @author Loganathan S - * - */ -@Component("scopeValidator") -public class ScopeValidator { - - private static final String SCOPE = "scope"; - - public static boolean hasAllScopes(List scopes) { - return hasScopes(scopes, Stream::allMatch); - } - - public static boolean hasAnyScopes(List scopes) { - return hasScopes(scopes, Stream::anyMatch); - } - - public static boolean hasScope(String scope) { - return hasAllScopes(List.of(scope)); - } - - public static boolean hasScopes(List scopes, BiPredicate, Predicate> condition) { - List scopesInToken = getScopes(); - return condition.test(scopes.stream(), scopesInToken::contains); - } - - private static List getScopes() { - Object principal = SecurityContextHolder - .getContext() - .getAuthentication().getPrincipal(); - if(principal instanceof AuthUserDetails) { - AuthUserDetails authUserDetails = (AuthUserDetails) principal; - String jwtToken = authUserDetails.getToken(); - DecodedJWT decodedJWT = JWT.decode(jwtToken); - String scpoeClaim = decodedJWT.getClaim(SCOPE).asString(); - List scopes = Stream.of( scpoeClaim.split(" ")).collect(Collectors.toList()); - return scopes; - } - - return List.of(); - } - - -} diff --git a/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/service/validator/ValidateTokenHelper.java b/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/service/validator/ValidateTokenHelper.java index cd0db8c39ff..8113d628907 100644 --- a/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/service/validator/ValidateTokenHelper.java +++ b/kernel/kernel-authcodeflowproxy-api/src/main/java/io/mosip/kernel/authcodeflowproxy/api/service/validator/ValidateTokenHelper.java @@ -31,6 +31,8 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.impl.NullClaim; +import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import io.mosip.kernel.authcodeflowproxy.api.constants.AuthConstant; @@ -101,8 +103,7 @@ public ImmutablePair isTokenValid(DecodedJWT decodedJWT) } // Second, issuer domain check. - boolean tokenDomainMatch = getTokenIssuerDomain(decodedJWT); - if (validateIssuerDomain && !tokenDomainMatch) { + if (validateIssuerDomain && !getTokenIssuerDomain(decodedJWT)) { LOGGER.error( "Provided Auth Token Issue domain does not match. Throwing Authentication Exception. UserName: " + userName); @@ -121,9 +122,8 @@ public ImmutablePair isTokenValid(DecodedJWT decodedJWT) } // Fourth, audience | azp validation. - boolean matchFound = validateAudience(decodedJWT); // No match found after comparing audience & azp - if (!matchFound) { + if (validateAudClaim && !validateAudience(decodedJWT)) { LOGGER.error("Provided Client Id does not match with Aud/AZP. Throwing Authorizaion Exception. UserName: " + userName); return ImmutablePair.of(Boolean.FALSE, AuthErrorCode.FORBIDDEN); @@ -132,18 +132,17 @@ public ImmutablePair isTokenValid(DecodedJWT decodedJWT) } private boolean validateAudience(DecodedJWT decodedJWT) { - boolean matchFound = false; - if (validateAudClaim) { + boolean matchFound; - List tokenAudience = decodedJWT.getAudience(); - matchFound = tokenAudience.stream().anyMatch(allowedAudience::contains); + List tokenAudience = decodedJWT.getAudience(); + matchFound = tokenAudience != null && tokenAudience.stream().anyMatch(allowedAudience::contains); - // comparing with azp. - String azp = decodedJWT.getClaim(AuthConstant.AZP).asString(); - if (!matchFound) { - matchFound = allowedAudience.stream().anyMatch(azp::equalsIgnoreCase); - } + // comparing with azp. + if (!matchFound) { + Claim azp = decodedJWT.getClaim(AuthConstant.AZP); + matchFound = azp != null && !(azp instanceof NullClaim) && allowedAudience.stream().anyMatch(azp.asString()::equalsIgnoreCase); } + return matchFound; } diff --git a/kernel/kernel-authcodeflowproxy-api/src/main/resources/META-INF/spring.factories b/kernel/kernel-authcodeflowproxy-api/src/main/resources/META-INF/spring.factories index ad32768515f..3230fb97de2 100644 --- a/kernel/kernel-authcodeflowproxy-api/src/main/resources/META-INF/spring.factories +++ b/kernel/kernel-authcodeflowproxy-api/src/main/resources/META-INF/spring.factories @@ -3,5 +3,4 @@ io.mosip.kernel.authcodeflowproxy.api.controller.LoginController,\ io.mosip.kernel.authcodeflowproxy.api.service.impl.LoginServiceImpl,\ io.mosip.kernel.authcodeflowproxy.api.config.AuthCodeProxyConfig,\ io.mosip.kernel.authcodeflowproxy.api.exception.AuthCodeProxyExceptionHandler,\ -io.mosip.kernel.authcodeflowproxy.api.service.validator.ValidateTokenHelper,\ -io.mosip.kernel.authcodeflowproxy.api.service.validator.ScopeValidator +io.mosip.kernel.authcodeflowproxy.api.service.validator.ValidateTokenHelper diff --git a/kernel/kernel-authcodeflowproxy-api/src/test/java/io/mosip/kernel/authcodeflowproxy/api/test/controller/AuthProxyControllerTests.java b/kernel/kernel-authcodeflowproxy-api/src/test/java/io/mosip/kernel/authcodeflowproxy/api/test/controller/AuthProxyControllerTests.java index e42a68e9ba6..bf865be9552 100644 --- a/kernel/kernel-authcodeflowproxy-api/src/test/java/io/mosip/kernel/authcodeflowproxy/api/test/controller/AuthProxyControllerTests.java +++ b/kernel/kernel-authcodeflowproxy-api/src/test/java/io/mosip/kernel/authcodeflowproxy/api/test/controller/AuthProxyControllerTests.java @@ -2,7 +2,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.isA; -import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; @@ -13,6 +13,7 @@ import java.net.URI; import java.time.Instant; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -20,20 +21,26 @@ import javax.servlet.http.Cookie; -import org.apache.commons.lang3.tuple.ImmutablePair; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.client.ExpectedCount; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.servlet.MockMvc; @@ -42,6 +49,7 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator.Builder; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.SignatureVerificationException; import com.fasterxml.jackson.databind.ObjectMapper; import io.mosip.kernel.authcodeflowproxy.api.constants.AuthConstant; @@ -54,12 +62,18 @@ import io.mosip.kernel.core.exception.ServiceError; import io.mosip.kernel.core.http.ResponseWrapper; import io.mosip.kernel.core.util.CryptoUtil; +import io.mosip.kernel.core.util.DateUtils; @SpringBootTest(classes = { AuthProxyFlowTestBootApplication.class }) -@RunWith(SpringRunner.class) @AutoConfigureMockMvc +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(SpringRunner.class) +@PowerMockIgnore({ "com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "javax.management.*", "com.sun.org.apache.xalan.*" }) +@PrepareForTest(Algorithm.class) public class AuthProxyControllerTests { + private static final int UNAUTHORIZED_STATUS = 401; + @Value("${auth.server.admin.validate.url}") private String validateUrl; @@ -71,13 +85,19 @@ public class AuthProxyControllerTests { private MockRestServiceServer mockServer; - @MockBean + @SpyBean private ValidateTokenHelper validateTokenHelper; + @Mock + private Algorithm mockAlgo; + @Before - public void init() { + public void init() throws Exception { mockServer = MockRestServiceServer.createServer(restTemplate); - when(validateTokenHelper.isTokenValid(Mockito.anyString())).thenReturn(ImmutablePair.of(true, null)); + PowerMockito.mockStatic(Algorithm.class); + when(Algorithm.RSA256(Mockito.any(), Mockito.any())).thenReturn(mockAlgo); + ReflectionTestUtils.setField(validateTokenHelper, "validateIssuerDomain", false); + ReflectionTestUtils.setField(validateTokenHelper, "validateAudClaim", false); } @Autowired @@ -190,8 +210,213 @@ public void loginTest() throws Exception { @Test public void loginRedirectTest() throws Exception { AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); - accessTokenResponse.setAccess_token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); - accessTokenResponse.setId_token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withClaim(AuthConstant.ISSUER, "http://localhost"); + + when(mockAlgo.getName()).thenReturn("RSA256"); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setId_token(token); + accessTokenResponse.setExpires_in("111"); + + mockServer + .expect(ExpectedCount.once(), + requestTo(new URI( + "http://localhost:8080/keycloak/auth/realms/mosip/protocol/openid-connect/token"))) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + + Cookie cookie = new Cookie("state", "mockstate"); + mockMvc.perform(get( + "/login-redirect/aHR0cDovL2xvY2FsaG9zdDo1MDAwLw==?state=mockstate&session_state=mock-session-state&code=mockcode") + .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) + .andExpect(status().is3xxRedirection()); + } + + @Test + public void loginRedirectTest_signatureVerification_negative() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withClaim(AuthConstant.ISSUER, "http://localhost"); + + when(mockAlgo.getName()).thenReturn("RSA256"); + doThrow(new SignatureVerificationException(mockAlgo)).when(mockAlgo).verify(Mockito.any()); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setExpires_in("111"); + + mockServer + .expect(ExpectedCount.once(), + requestTo(new URI( + "http://localhost:8080/keycloak/auth/realms/mosip/protocol/openid-connect/token"))) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + + Cookie cookie = new Cookie("state", "mockstate"); + mockMvc.perform(get( + "/login-redirect/aHR0cDovL2xvY2FsaG9zdDo1MDAwLw==?state=mockstate&session_state=mock-session-state&code=mockcode") + .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) + .andExpect(status().is(UNAUTHORIZED_STATUS)); + } + + @Test + public void loginRedirectTest_expiredToken() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().minusDays(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withClaim(AuthConstant.ISSUER, "http://localhost"); + + when(mockAlgo.getName()).thenReturn("RSA256"); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setExpires_in("111"); + + mockServer + .expect(ExpectedCount.once(), + requestTo(new URI( + "http://localhost:8080/keycloak/auth/realms/mosip/protocol/openid-connect/token"))) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + + Cookie cookie = new Cookie("state", "mockstate"); + mockMvc.perform(get( + "/login-redirect/aHR0cDovL2xvY2FsaG9zdDo1MDAwLw==?state=mockstate&session_state=mock-session-state&code=mockcode") + .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) + .andExpect(status().is(401)); + } + + @Test + public void loginRedirectTest_domain_match_positive() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withClaim(AuthConstant.ISSUER, "http://localhost"); + + when(mockAlgo.getName()).thenReturn("RSA256"); + ReflectionTestUtils.setField(validateTokenHelper, "validateIssuerDomain", true); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setId_token(token); + accessTokenResponse.setExpires_in("111"); + + mockServer + .expect(ExpectedCount.once(), + requestTo(new URI( + "http://localhost:8080/keycloak/auth/realms/mosip/protocol/openid-connect/token"))) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + + Cookie cookie = new Cookie("state", "mockstate"); + mockMvc.perform(get( + "/login-redirect/aHR0cDovL2xvY2FsaG9zdDo1MDAwLw==?state=mockstate&session_state=mock-session-state&code=mockcode") + .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) + .andExpect(status().is3xxRedirection()); + } + + @Test + public void loginRedirectTest_invalid_issuer() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withClaim(AuthConstant.ISSUER, "~!::#@///wrongurl"); + ReflectionTestUtils.setField(validateTokenHelper, "validateIssuerDomain", true); + + when(mockAlgo.getName()).thenReturn("RSA256"); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setExpires_in("111"); + + mockServer + .expect(ExpectedCount.once(), + requestTo(new URI( + "http://localhost:8080/keycloak/auth/realms/mosip/protocol/openid-connect/token"))) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + + Cookie cookie = new Cookie("state", "mockstate"); + mockMvc.perform(get( + "/login-redirect/aHR0cDovL2xvY2FsaG9zdDo1MDAwLw==?state=mockstate&session_state=mock-session-state&code=mockcode") + .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) + .andExpect(status().is(401)); + } + + @Test + public void loginRedirectTest_domain_match_negative() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withClaim(AuthConstant.ISSUER, "http://someotherdomain"); + + when(mockAlgo.getName()).thenReturn("RSA256"); + ReflectionTestUtils.setField(validateTokenHelper, "validateIssuerDomain", true); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setExpires_in("111"); + + mockServer + .expect(ExpectedCount.once(), + requestTo(new URI( + "http://localhost:8080/keycloak/auth/realms/mosip/protocol/openid-connect/token"))) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + + Cookie cookie = new Cookie("state", "mockstate"); + mockMvc.perform(get( + "/login-redirect/aHR0cDovL2xvY2FsaG9zdDo1MDAwLw==?state=mockstate&session_state=mock-session-state&code=mockcode") + .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) + .andExpect(status().is(401)); + } + + @Test + public void loginRedirectTest_aud_match_positive() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withAudience("myapp-client"); + + when(mockAlgo.getName()).thenReturn("RSA256"); + ReflectionTestUtils.setField(validateTokenHelper, "validateAudClaim", true); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setId_token(token); + accessTokenResponse.setExpires_in("111"); + + mockServer + .expect(ExpectedCount.once(), + requestTo(new URI( + "http://localhost:8080/keycloak/auth/realms/mosip/protocol/openid-connect/token"))) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + + Cookie cookie = new Cookie("state", "mockstate"); + mockMvc.perform(get( + "/login-redirect/aHR0cDovL2xvY2FsaG9zdDo1MDAwLw==?state=mockstate&session_state=mock-session-state&code=mockcode") + .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) + .andExpect(status().is3xxRedirection()); + } + + @Test + public void loginRedirectTest_aud_match_negative_azp_positive() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withAudience("somether-app-client"); + withExpiresAt.withClaim(AuthConstant.AZP, "myapp-client"); + + when(mockAlgo.getName()).thenReturn("RSA256"); + ReflectionTestUtils.setField(validateTokenHelper, "validateAudClaim", true); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setId_token(token); accessTokenResponse.setExpires_in("111"); mockServer @@ -208,6 +433,62 @@ public void loginRedirectTest() throws Exception { .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) .andExpect(status().is3xxRedirection()); } + + @Test + public void loginRedirectTest_aud_match_null_azp_null() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + + when(mockAlgo.getName()).thenReturn("RSA256"); + ReflectionTestUtils.setField(validateTokenHelper, "validateAudClaim", true); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setExpires_in("111"); + + mockServer + .expect(ExpectedCount.once(), + requestTo(new URI( + "http://localhost:8080/keycloak/auth/realms/mosip/protocol/openid-connect/token"))) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + + Cookie cookie = new Cookie("state", "mockstate"); + mockMvc.perform(get( + "/login-redirect/aHR0cDovL2xvY2FsaG9zdDo1MDAwLw==?state=mockstate&session_state=mock-session-state&code=mockcode") + .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) + .andExpect(status().is(401)); + } + + @Test + public void loginRedirectTest_aud_match_negative_azp_negative() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withAudience("someother-app-client"); + withExpiresAt.withClaim(AuthConstant.AZP, "someother-app-client"); + + when(mockAlgo.getName()).thenReturn("RSA256"); + ReflectionTestUtils.setField(validateTokenHelper, "validateAudClaim", true); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setExpires_in("111"); + + mockServer + .expect(ExpectedCount.once(), + requestTo(new URI( + "http://localhost:8080/keycloak/auth/realms/mosip/protocol/openid-connect/token"))) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + + Cookie cookie = new Cookie("state", "mockstate"); + mockMvc.perform(get( + "/login-redirect/aHR0cDovL2xvY2FsaG9zdDo1MDAwLw==?state=mockstate&session_state=mock-session-state&code=mockcode") + .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) + .andExpect(status().is(401)); + } @Test public void loginRedirectTestWithHash() throws Exception { @@ -225,7 +506,7 @@ public void loginRedirectTestWithHash() throws Exception { accessTokenResponse.setAccess_token(jwtToken); accessTokenResponse.setId_token(jwtToken); accessTokenResponse.setExpires_in("111"); - + mockServer .expect(ExpectedCount.once(), requestTo(new URI( @@ -296,8 +577,14 @@ public void loginInvalidUUIDTest() throws Exception { @Test public void logoutRedirectHostCheckTest() throws Exception { AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); - accessTokenResponse.setAccess_token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); - accessTokenResponse.setId_token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + Builder withExpiresAt = JWT.create().withExpiresAt(Date.from(DateUtils.getUTCCurrentDateTime().plusHours(1).toInstant(ZoneOffset.UTC))); + withExpiresAt.withClaim(AuthConstant.ISSUER, "http://localhost"); + + when(mockAlgo.getName()).thenReturn("RSA256"); + String token = withExpiresAt.withClaim("scope", "aaa bbb").sign(mockAlgo); + + accessTokenResponse.setAccess_token(token); + accessTokenResponse.setId_token(token); accessTokenResponse.setExpires_in("111"); mockServer @@ -313,7 +600,7 @@ public void logoutRedirectHostCheckTest() throws Exception { .contentType(MediaType.APPLICATION_JSON).cookie(cookie)) .andExpect(status().isOk()) .andExpect(jsonPath("$.errors[0].errorCode", is(Errors.ALLOWED_URL_EXCEPTION.getErrorCode()))); - ; + } } diff --git a/kernel/kernel-authcodeflowproxy-api/src/test/resources/application-test.properties b/kernel/kernel-authcodeflowproxy-api/src/test/resources/application-test.properties index 247296b4c58..250dc076b9d 100644 --- a/kernel/kernel-authcodeflowproxy-api/src/test/resources/application-test.properties +++ b/kernel/kernel-authcodeflowproxy-api/src/test/resources/application-test.properties @@ -46,3 +46,5 @@ auth.server.admin.audience.claim.validate=false mosip.iam.certs_endpoint=http://localhost:5000/keycloak/auth/realms/mosip/protocol/openid-connect/certs +auth.server.admin.allowed.audience=myapp-client + diff --git a/kernel/kernel-core/src/main/java/io/mosip/kernel/core/authmanager/authadapter/model/AuthUserDetails.java b/kernel/kernel-core/src/main/java/io/mosip/kernel/core/authmanager/authadapter/model/AuthUserDetails.java index 0dce94919bf..00e2e87f779 100644 --- a/kernel/kernel-core/src/main/java/io/mosip/kernel/core/authmanager/authadapter/model/AuthUserDetails.java +++ b/kernel/kernel-core/src/main/java/io/mosip/kernel/core/authmanager/authadapter/model/AuthUserDetails.java @@ -1,7 +1,9 @@ package io.mosip.kernel.core.authmanager.authadapter.model; import java.util.Collection; +import java.util.Collections; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -18,6 +20,10 @@ public class AuthUserDetails implements UserDetails { + public static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_"; + + public static final String ROLE_AUTHORITY_PREFIX = "ROLE_"; + /** * */ @@ -41,12 +47,30 @@ public AuthUserDetails(MosipUserDto mosipUserDto, String token) { @Override public Collection getAuthorities() { - return authorities.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role.getAuthority())) - .collect(Collectors.toList()); - } - - public void setAuthorities(Collection authorities) { - this.authorities = authorities; + return authorities; + } + + private void addAuthorities(Collection authorities, String authorityPrefix) { + Stream authortiesStream = authorities.stream().map(grantedAuthority -> { + String authority = authorityPrefix == null ? grantedAuthority.getAuthority() : authorityPrefix + grantedAuthority.getAuthority(); + return new SimpleGrantedAuthority(authority); + }); + + if(this.authorities == null) { + this.authorities = Collections.unmodifiableCollection(authortiesStream + .collect(Collectors.toList())); + } else { + this.authorities = Collections.unmodifiableCollection(Stream.concat(this.authorities.stream(), authortiesStream) + .collect(Collectors.toList())); + } + } + + public void addRoleAuthorities(Collection authorities) { + this.addAuthorities(authorities, ROLE_AUTHORITY_PREFIX); + } + + public void addScopeAuthorities(Collection authorities) { + this.addAuthorities(authorities, SCOPE_AUTHORITY_PREFIX); } @Override diff --git a/kernel/kernel-core/src/main/java/io/mosip/kernel/core/authmanager/spi/ScopeValidator.java b/kernel/kernel-core/src/main/java/io/mosip/kernel/core/authmanager/spi/ScopeValidator.java new file mode 100644 index 00000000000..412073f27b1 --- /dev/null +++ b/kernel/kernel-core/src/main/java/io/mosip/kernel/core/authmanager/spi/ScopeValidator.java @@ -0,0 +1,23 @@ +package io.mosip.kernel.core.authmanager.spi; + +import java.util.List; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.stream.Stream; +/** + * Validator used to validate the scope in the token + * + * @author Loganathan S + * + */ +public interface ScopeValidator { + + public boolean hasAllScopes(List scopes); + + public boolean hasAnyScopes(List scopes); + + public boolean hasScope(String scope); + + public boolean hasScopes(List scopes, BiPredicate, Predicate> condition); + +}