From fcd81a2fd031adce262d2b4fbaefcfa3b752057d Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Fri, 26 Jul 2024 11:10:46 +0200 Subject: [PATCH 01/19] Fix #530: Set develop version to 1.9.0-SNAPSHOT --- pom.xml | 8 ++++---- powerauth-restful-model/pom.xml | 2 +- powerauth-restful-security-spring-annotation/pom.xml | 2 +- powerauth-restful-security-spring/pom.xml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 4e68bbcd..f76ce3c1 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ io.getlime.security powerauth-restful-integration-parent - 1.8.0 + 1.9.0-SNAPSHOT pom 2017 @@ -87,9 +87,9 @@ 3.3.2 1.12.0 - 1.10.0 - 1.8.0 - 1.8.0 + 1.11.0-SNAPSHOT + 1.9.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/powerauth-restful-model/pom.xml b/powerauth-restful-model/pom.xml index 390bd8a4..fc06e24f 100644 --- a/powerauth-restful-model/pom.xml +++ b/powerauth-restful-model/pom.xml @@ -30,7 +30,7 @@ io.getlime.security powerauth-restful-integration-parent - 1.8.0 + 1.9.0-SNAPSHOT diff --git a/powerauth-restful-security-spring-annotation/pom.xml b/powerauth-restful-security-spring-annotation/pom.xml index 8fb06e25..27ad4347 100644 --- a/powerauth-restful-security-spring-annotation/pom.xml +++ b/powerauth-restful-security-spring-annotation/pom.xml @@ -30,7 +30,7 @@ io.getlime.security powerauth-restful-integration-parent - 1.8.0 + 1.9.0-SNAPSHOT diff --git a/powerauth-restful-security-spring/pom.xml b/powerauth-restful-security-spring/pom.xml index 8da451c8..a97691aa 100644 --- a/powerauth-restful-security-spring/pom.xml +++ b/powerauth-restful-security-spring/pom.xml @@ -30,7 +30,7 @@ io.getlime.security powerauth-restful-integration-parent - 1.8.0 + 1.9.0-SNAPSHOT From 68cee54a256a6ce34d35a6c2fb717a0b8363f3cb Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Tue, 6 Aug 2024 07:02:17 +0200 Subject: [PATCH 02/19] Fix #540: Cleanup usage of RequestMapping annotations --- docs/RESTful-API-for-Spring.md | 41 +++++++++---------- .../controller/ActivationController.java | 2 +- .../spring/controller/RecoveryController.java | 2 +- .../controller/SecureVaultController.java | 2 +- .../controller/SignatureController.java | 2 +- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/docs/RESTful-API-for-Spring.md b/docs/RESTful-API-for-Spring.md index 99c67405..42a44ae6 100644 --- a/docs/RESTful-API-for-Spring.md +++ b/docs/RESTful-API-for-Spring.md @@ -231,13 +231,12 @@ Note: Controllers that establish a session must not be on a context that is prot ```java -@Controller -@RequestMapping(value = "session") +@RestController +@RequestMapping("session") public class AuthenticationController { - @RequestMapping(value = "login", method = RequestMethod.POST) + @PostMapping("login") @PowerAuth(resourceId = "/session/login") - @ResponseBody public MyApiResponse login(PowerAuthApiAuthentication auth) { if (auth == null) { // handle authentication failure @@ -265,13 +264,12 @@ In case both `@RequestParam` and `@PathVariable` with the same name exist, the v Example of using dynamic resource ID: ```java -@Controller -@RequestMapping(value = "secured") +@RestController +@RequestMapping("secured") public class AuthenticationController { - @RequestMapping(value = "account/{id}", method = RequestMethod.POST) + @PostMapping("account/{id}") @PowerAuth(resourceId = "/secured/account/${id}?filter=${filter}") - @ResponseBody public MyAccountApiResponse changeAccountSettings( @PathVariable("id") String accountId, @RequestParam("filter") String filter, PowerAuthApiAuthentication auth, PowerAuthActivation activation) { @@ -296,15 +294,14 @@ public class AuthenticationController { In case you need a more low-level access to the signature verification, you can verify the signature manually using the `PowerAuthAuthenticationProvider` like this: ```java -@Controller -@RequestMapping(value = "session") +@RestController +@RequestMapping("session") public class AuthenticationController { @Autowired private PowerAuthAuthenticationProvider authenticationProvider; - @RequestMapping(value = "login", method = RequestMethod.POST) - @ResponseBody + @PostMapping("login") public ObjectResponse login( @RequestHeader(value = PowerAuthSignatureHttpHeader.HEADER_NAME, required = true) String signatureHeader, HttpServletRequest servletRequest) throws Exception { @@ -357,16 +354,16 @@ This sample `@Controller` implementation illustrates how to use `@PowerAuthToken Please note that token based authentication should be used only for endpoints with lower sensitivity, such as simplified account information for widgets or smart watch, that are also not prone to replay attack. ```java -@Controller -@RequestMapping(value = "secure/account") +@RestController +@RequestMapping("secure/account") public class AuthenticationController { @Autowired private CustomService service; - @RequestMapping(value = "widget/balance", method = RequestMethod.GET) + @GetMapping("widget/balance") @PowerAuthToken - public @ResponseBody ObjectResponse getBalance(PowerAuthApiAuthentication apiAuthentication) throws PowerAuthAuthenticationException { + public ObjectResponse getBalance(PowerAuthApiAuthentication apiAuthentication) throws PowerAuthAuthenticationException { if (apiAuthentication == null) { throw new PowerAuthTokenInvalidException(); } else { @@ -391,10 +388,10 @@ You can encrypt data in `application` scope (non-personalized) using following p ```java @RestController -@RequestMapping(value = "/exchange") +@RequestMapping("/exchange") public class EncryptedDataExchangeController { - @RequestMapping(value = "application", method = RequestMethod.POST) + @PostMapping("application") @PowerAuthEncryption(scope = EncryptionScope.APPLICATION_SCOPE) public DataExchangeResponse exchangeInApplicationScope(@EncryptedRequestBody DataExchangeRequest request, EncryptionContext encryptionContext) throws PowerAuthEncryptionException { @@ -419,10 +416,10 @@ You can encrypt data in `activation` scope (personalized) using following patter ```java @RestController -@RequestMapping(value = "/exchange") +@RequestMapping("/exchange") public class EncryptedDataExchangeController { - @RequestMapping(value = "activation", method = RequestMethod.POST) + @PostMapping("activation") @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) public DataExchangeResponse exchangeInActivationScope(@EncryptedRequestBody DataExchangeRequest request, EncryptionContext encryptionContext) throws PowerAuthEncryptionException { @@ -447,10 +444,10 @@ You can also sign the data before encryption and perform signature verification ```java @RestController -@RequestMapping(value = "/exchange") +@RequestMapping("/exchange") public class EncryptedDataExchangeController { - @RequestMapping(value = "signed", method = RequestMethod.POST) + @PostMapping("signed") @PowerAuth(resourceId = "/exchange/signed") @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) public DataExchangeResponse exchangeSignedAndEncryptedData(@EncryptedRequestBody DataExchangeRequest request, diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ActivationController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ActivationController.java index 19613619..72853684 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ActivationController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ActivationController.java @@ -66,7 +66,7 @@ * */ @RestController("activationControllerV3") -@RequestMapping(value = "/pa/v3/activation") +@RequestMapping("/pa/v3/activation") public class ActivationController { private static final Logger logger = LoggerFactory.getLogger(ActivationController.class); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/RecoveryController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/RecoveryController.java index 0e97c72c..fb208457 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/RecoveryController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/RecoveryController.java @@ -50,7 +50,7 @@ * */ @RestController -@RequestMapping(value = "/pa/v3/recovery") +@RequestMapping("/pa/v3/recovery") public class RecoveryController { private static final Logger logger = LoggerFactory.getLogger(RecoveryController.class); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SecureVaultController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SecureVaultController.java index 875c6cc6..72089db0 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SecureVaultController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SecureVaultController.java @@ -49,7 +49,7 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @RestController("secureVaultControllerV3") -@RequestMapping(value = "/pa/v3/vault") +@RequestMapping("/pa/v3/vault") public class SecureVaultController { private static final Logger logger = LoggerFactory.getLogger(SecureVaultController.class); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SignatureController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SignatureController.java index c54a8645..a3f3b7a8 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SignatureController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SignatureController.java @@ -42,7 +42,7 @@ * */ @RestController("signatureControllerV3") -@RequestMapping(value = "/pa/v3/signature") +@RequestMapping("/pa/v3/signature") public class SignatureController { /** From 115fbd88b72e484763ec973a0c61306f738cf10b Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Wed, 7 Aug 2024 14:58:17 +0200 Subject: [PATCH 03/19] Fix #542: Reduce duplicity and improve exception handling in ActivationService --- .../PowerAuthActivationException.java | 10 ++++ .../api/spring/service/ActivationService.java | 55 +++++++------------ 2 files changed, 29 insertions(+), 36 deletions(-) diff --git a/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthActivationException.java b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthActivationException.java index 13dbf5ea..a80e6d77 100644 --- a/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthActivationException.java +++ b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthActivationException.java @@ -57,6 +57,16 @@ public PowerAuthActivationException(Throwable cause) { super(cause); } + /** + * Constructor with a message and a cause. + * + * @param message Error message. + * @param cause Error cause. + */ + public PowerAuthActivationException(final String message, final Throwable cause) { + super(message, cause); + } + /** * Get default error code, used for example in the REST response. * @return Default error code. diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java index 9613d9ac..1e408ddd 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java @@ -27,7 +27,10 @@ import com.wultra.security.powerauth.client.model.response.*; import io.getlime.security.powerauth.rest.api.model.entity.ActivationType; import io.getlime.security.powerauth.rest.api.model.entity.UserInfoStage; -import io.getlime.security.powerauth.rest.api.model.request.*; +import io.getlime.security.powerauth.rest.api.model.request.ActivationLayer1Request; +import io.getlime.security.powerauth.rest.api.model.request.ActivationRenameRequest; +import io.getlime.security.powerauth.rest.api.model.request.ActivationStatusRequest; +import io.getlime.security.powerauth.rest.api.model.request.EciesEncryptedRequest; import io.getlime.security.powerauth.rest.api.model.response.*; import io.getlime.security.powerauth.rest.api.spring.application.PowerAuthApplicationConfiguration; import io.getlime.security.powerauth.rest.api.spring.authentication.PowerAuthApiAuthentication; @@ -44,6 +47,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.time.Instant; @@ -123,8 +127,11 @@ public void setUserInfoProvider(UserInfoProvider userInfoProvider) { * @throws PowerAuthRecoveryException In case activation recovery fails. */ public ActivationLayer1Response createActivation(ActivationLayer1Request request, EncryptionContext eciesContext) throws PowerAuthActivationException, PowerAuthRecoveryException { - try { + final Map identity = request.getIdentityAttributes(); + + checkIdentityAttributesPresent(identity); + try { final String applicationKey = eciesContext.getApplicationKey(); final EciesEncryptedRequest activationData = request.getActivationData(); final String ephemeralPublicKey = activationData.getEphemeralPublicKey(); @@ -132,19 +139,12 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request final String mac = activationData.getMac(); final String nonce = activationData.getNonce(); final Long timestamp = activationData.getTimestamp(); - final Map identity = request.getIdentityAttributes(); final Map customAttributes = (request.getCustomAttributes() != null) ? request.getCustomAttributes() : new HashMap<>(); switch (request.getType()) { // Regular activation which uses "code" identity attribute case CODE -> { - // Check if identity attributes are present - if (identity == null || identity.isEmpty()) { - logger.warn("Identity attributes are missing for code activation"); - throw new PowerAuthActivationException(); - } - // Extract data from request and encryption object final String activationCode = identity.get("code"); @@ -247,12 +247,6 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request throw new PowerAuthActivationException(); } - // Check if identity attributes are present - if (identity == null || identity.isEmpty()) { - logger.warn("Identity attributes are missing for custom activation"); - throw new PowerAuthActivationException(); - } - // Create context for passing parameters between activation provider calls final Map context = new LinkedHashMap<>(); @@ -354,12 +348,6 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request // Activation using recovery code case RECOVERY -> { - // Check if identity attributes are present - if (identity == null || identity.isEmpty()) { - logger.warn("Identity attributes are missing for activation recovery"); - throw new PowerAuthActivationException(); - } - // Extract data from request and encryption object final String recoveryCode = identity.get("recoveryCode"); final String recoveryPuk = identity.get("puk"); @@ -459,28 +447,23 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request return prepareEncryptedResponse(response.getEncryptedData(), response.getMac(), response.getNonce(), response.getTimestamp(), processedCustomAttributes, userInfo); } - default -> { - logger.warn("Invalid activation request"); - throw new PowerAuthInvalidRequestException(); - } + default -> + throw new PowerAuthInvalidRequestException("Unsupported activation type: " + request.getType()); } } catch (PowerAuthClientException ex) { if (ex.getPowerAuthError().orElse(null) instanceof final PowerAuthErrorRecovery errorRecovery) { logger.debug("Invalid recovery code, current PUK index: {}", errorRecovery.getCurrentRecoveryPukIndex()); throw new PowerAuthRecoveryException(ex.getMessage(), "INVALID_RECOVERY_CODE", errorRecovery.getCurrentRecoveryPukIndex()); } - logger.warn("Creating PowerAuth activation failed, error: {}", ex.getMessage()); - logger.debug(ex.getMessage(), ex); - throw new PowerAuthActivationException(); - } catch (PowerAuthActivationException ex) { - // Do not swallow PowerAuthActivationException for custom activations. - // See: https://github.com/wultra/powerauth-restful-integration/issues/199 - logger.warn("Creating PowerAuth activation failed, error: {}", ex.getMessage()); - throw ex; + throw new PowerAuthActivationException("Creating PowerAuth activation failed.", ex); } catch (Exception ex) { - logger.warn("Creating PowerAuth activation failed, error: {}", ex.getMessage()); - logger.debug(ex.getMessage(), ex); - throw new PowerAuthActivationException(); + throw new PowerAuthActivationException("Creating PowerAuth activation failed.", ex); + } + } + + private static void checkIdentityAttributesPresent(final Map identity) throws PowerAuthActivationException { + if (CollectionUtils.isEmpty(identity)) { + throw new PowerAuthActivationException("Identity attributes are missing for activation."); } } From bf309b65da47309108572d4d39901f4d055ffd6e Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Thu, 8 Aug 2024 13:18:29 +0200 Subject: [PATCH 04/19] Fix #544: Extract methods commitActivation and fetchUserClaimsForUserId in ActivationService --- .../api/spring/service/ActivationService.java | 106 +++++++----------- 1 file changed, 42 insertions(+), 64 deletions(-) diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java index 9613d9ac..384b353f 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java @@ -177,19 +177,13 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request final String activationId = response.getActivationId(); final String applicationId = response.getApplicationId(); - // Process user info - Map userInfo = null; - if (userInfoProvider != null) { - final UserInfoContext userInfoContext = UserInfoContext.builder() - .stage(UserInfoStage.ACTIVATION_PROCESS_ACTIVATION_CODE) - .userId(userId) - .activationId(activationId) - .applicationId(applicationId) - .build(); - if (userInfoProvider.shouldReturnUserInfo(userInfoContext)) { - userInfo = userInfoProvider.fetchUserClaimsForUserId(userInfoContext); - } - } + final UserInfoContext userInfoContext = UserInfoContext.builder() + .stage(UserInfoStage.ACTIVATION_PROCESS_ACTIVATION_CODE) + .userId(userId) + .activationId(activationId) + .applicationId(applicationId) + .build(); + final Map userInfo = processUserInfo(userInfoContext); Map processedCustomAttributes = customAttributes; // In case a custom activation provider is enabled, process custom attributes and save any flags @@ -215,15 +209,7 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request } else { // Otherwise check if activation should be committed instantly and if yes, perform commit. if (activationProvider != null && activationProvider.shouldAutoCommitActivation(identity, customAttributes, activationId, userId, applicationId, ActivationType.CODE, context)) { - final CommitActivationRequest commitRequest = new CommitActivationRequest(); - commitRequest.setActivationId(activationId); - commitRequest.setExternalUserId(null); - final CommitActivationResponse commitResponse = powerAuthClient.commitActivation( - commitRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - + final CommitActivationResponse commitResponse = commitActivation(activationId); notifyActivationCommit = commitResponse.isActivated(); } } @@ -300,19 +286,13 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request final String activationId = response.getActivationId(); final String applicationId = response.getApplicationId(); - // Process user info - Map userInfo = null; - if (userInfoProvider != null) { - final UserInfoContext userInfoContext = UserInfoContext.builder() - .stage(UserInfoStage.ACTIVATION_PROCESS_CUSTOM) - .userId(userId) - .activationId(activationId) - .applicationId(applicationId) - .build(); - if (userInfoProvider.shouldReturnUserInfo(userInfoContext)) { - userInfo = userInfoProvider.fetchUserClaimsForUserId(userInfoContext); - } - } + final UserInfoContext userInfoContext = UserInfoContext.builder() + .stage(UserInfoStage.ACTIVATION_PROCESS_CUSTOM) + .userId(userId) + .activationId(activationId) + .applicationId(applicationId) + .build(); + final Map userInfo = processUserInfo(userInfoContext); // Process custom attributes using a custom logic final Map processedCustomAttributes = activationProvider.processCustomActivationAttributes(customAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context); @@ -332,14 +312,7 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request // Check if activation should be committed instantly and if yes, perform commit if (activationProvider.shouldAutoCommitActivation(identity, customAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context)) { - final CommitActivationRequest commitRequest = new CommitActivationRequest(); - commitRequest.setActivationId(activationId); - commitRequest.setExternalUserId(null); - final CommitActivationResponse commitResponse = powerAuthClient.commitActivation( - commitRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); + final CommitActivationResponse commitResponse = commitActivation(activationId); if (commitResponse.isActivated()) { activationProvider.activationWasCommitted(identity, customAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context); } @@ -409,19 +382,13 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request final String activationId = response.getActivationId(); final String applicationId = response.getApplicationId(); - // Process user info - Map userInfo = null; - if (userInfoProvider != null) { - final UserInfoContext userInfoContext = UserInfoContext.builder() - .stage(UserInfoStage.ACTIVATION_PROCESS_RECOVERY) - .userId(userId) - .activationId(activationId) - .applicationId(applicationId) - .build(); - if (userInfoProvider.shouldReturnUserInfo(userInfoContext)) { - userInfo = userInfoProvider.fetchUserClaimsForUserId(userInfoContext); - } - } + final UserInfoContext userInfoContext = UserInfoContext.builder() + .stage(UserInfoStage.ACTIVATION_PROCESS_RECOVERY) + .userId(userId) + .activationId(activationId) + .applicationId(applicationId) + .build(); + final Map userInfo = processUserInfo(userInfoContext); Map processedCustomAttributes = customAttributes; // In case a custom activation provider is enabled, process custom attributes and save any flags @@ -442,14 +409,7 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request // Automatically commit activation by default, the optional activation provider can override automatic commit if (activationProvider == null || activationProvider.shouldAutoCommitActivation(identity, customAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context)) { - final CommitActivationRequest commitRequest = new CommitActivationRequest(); - commitRequest.setActivationId(activationId); - commitRequest.setExternalUserId(null); - final CommitActivationResponse commitResponse = powerAuthClient.commitActivation( - commitRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); + final CommitActivationResponse commitResponse = commitActivation(activationId); if (activationProvider != null && commitResponse.isActivated()) { activationProvider.activationWasCommitted(identity, customAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context); } @@ -484,6 +444,24 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request } } + private CommitActivationResponse commitActivation(final String activationId) throws PowerAuthClientException { + final CommitActivationRequest commitRequest = new CommitActivationRequest(); + commitRequest.setActivationId(activationId); + commitRequest.setExternalUserId(null); + return powerAuthClient.commitActivation( + commitRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + } + + private Map processUserInfo(final UserInfoContext userInfoContext) { + if (userInfoProvider != null && userInfoProvider.shouldReturnUserInfo(userInfoContext)) { + return userInfoProvider.fetchUserClaimsForUserId(userInfoContext); + } + return null; + } + private boolean shouldGenerateRecoveryCodes(final Map identity, final Map customAttributes, final Map context) throws PowerAuthActivationException { if (activationProvider == null) { return true; From 699bea9c20261cb31742b6e05db1454ac7ed4b64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 07:50:58 +0000 Subject: [PATCH 05/19] Bump org.springframework.boot:spring-boot-dependencies Bumps [org.springframework.boot:spring-boot-dependencies](https://github.com/spring-projects/spring-boot) from 3.3.2 to 3.3.3. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.2...v3.3.3) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f76ce3c1..0e7b30c2 100644 --- a/pom.xml +++ b/pom.xml @@ -84,7 +84,7 @@ 3.8.0 3.3.1 3.4.0 - 3.3.2 + 3.3.3 1.12.0 1.11.0-SNAPSHOT From d64ddf130255a22e955e76c7892e9f8ac2ffa7f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 07:51:03 +0000 Subject: [PATCH 06/19] Bump org.apache.maven.plugins:maven-deploy-plugin from 3.1.2 to 3.1.3 Bumps [org.apache.maven.plugins:maven-deploy-plugin](https://github.com/apache/maven-deploy-plugin) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/apache/maven-deploy-plugin/releases) - [Commits](https://github.com/apache/maven-deploy-plugin/compare/maven-deploy-plugin-3.1.2...maven-deploy-plugin-3.1.3) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-deploy-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f76ce3c1..4c9283af 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ 17 ${java.version} ${java.version} - 3.1.2 + 3.1.3 3.5.0 3.4.2 3.8.0 From 2c1ac1ebaf07dc98d1d586e3799eef123e1a2f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Dvo=C5=99=C3=A1k?= Date: Wed, 4 Sep 2024 17:14:11 +0200 Subject: [PATCH 07/19] Implement support for temporary keys (#546) * Add temporary key ID to service calls * Added endpoints for requesting temporary key * Fix exception and controller bean names * Move temporary key ID to the request body * Fix supported version support * Removed unnecessary annotation * Add version to status endpoint if available * Improve the structure of the application info in status endpoint * Add request/response wrapper --- .../model/request/EciesEncryptedRequest.java | 5 ++ .../model/request/TemporaryKeyRequest.java | 37 ++++++++ .../model/response/ServerStatusResponse.java | 9 +- .../model/response/TemporaryKeyResponse.java | 37 ++++++++ .../spring/encryption/EncryptionContext.java | 1 + .../PowerAuthTemporaryKeyException.java | 55 ++++++++++++ .../provider/PowerAuthEncryptionProvider.java | 3 +- .../PowerAuthEncryptionProviderBase.java | 6 +- .../controller/ActivationController.java | 3 + .../spring/controller/KeyStoreController.java | 85 +++++++++++++++++++ .../spring/controller/RecoveryController.java | 6 +- .../controller/SecureVaultController.java | 7 +- .../controller/ServerStatusController.java | 22 ++++- .../controller/SignatureController.java | 3 + .../spring/controller/TokenController.java | 6 +- .../spring/controller/UpgradeController.java | 7 +- .../spring/controller/UserInfoController.java | 1 + .../exception/PowerAuthExceptionHandler.java | 12 +++ .../api/spring/service/ActivationService.java | 4 + .../api/spring/service/KeyStoreService.java | 84 ++++++++++++++++++ .../api/spring/service/RecoveryService.java | 1 + .../spring/service/SecureVaultService.java | 1 + .../rest/api/spring/service/TokenService.java | 1 + .../api/spring/service/UpgradeService.java | 1 + .../api/spring/util/PowerAuthVersionUtil.java | 57 ++++++++++++- .../api/spring/PowerAuthVersionUtilTest.java | 10 +++ 26 files changed, 449 insertions(+), 15 deletions(-) create mode 100644 powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/request/TemporaryKeyRequest.java create mode 100644 powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/response/TemporaryKeyResponse.java create mode 100644 powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthTemporaryKeyException.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/KeyStoreController.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/KeyStoreService.java diff --git a/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/request/EciesEncryptedRequest.java b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/request/EciesEncryptedRequest.java index fadd11dd..827b7b9b 100644 --- a/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/request/EciesEncryptedRequest.java +++ b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/request/EciesEncryptedRequest.java @@ -30,6 +30,11 @@ @Data public class EciesEncryptedRequest { + /** + * Identifier of the temporary key. + */ + private String temporaryKeyId; + /** * Base64 encoded ephemeral public key. */ diff --git a/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/request/TemporaryKeyRequest.java b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/request/TemporaryKeyRequest.java new file mode 100644 index 00000000..5df0ebd4 --- /dev/null +++ b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/request/TemporaryKeyRequest.java @@ -0,0 +1,37 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.model.request; + +import lombok.Data; + +/** + * Request class with temporary public key. + * + * @author Petr Dvorak, petr@wultra.com + */ +@Data +public class TemporaryKeyRequest { + + /** + * JWT with encoded temporary key request. + */ + private String jwt; + +} \ No newline at end of file diff --git a/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/response/ServerStatusResponse.java b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/response/ServerStatusResponse.java index 16cb7eff..291c4db4 100644 --- a/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/response/ServerStatusResponse.java +++ b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/response/ServerStatusResponse.java @@ -25,5 +25,12 @@ * @param serverTime Server time. * @author Roman Strobl, roman.strobl@wultra.com */ -public record ServerStatusResponse(long serverTime) { +public record ServerStatusResponse(long serverTime, Application application) { + /** + * Record for information about the application. + * @param name Application name, if present in BuildProperties. + * @param version Application version, if present in BuildProperties. + */ + public record Application(String name, String version) { + } } \ No newline at end of file diff --git a/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/response/TemporaryKeyResponse.java b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/response/TemporaryKeyResponse.java new file mode 100644 index 00000000..8dd1f85e --- /dev/null +++ b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/response/TemporaryKeyResponse.java @@ -0,0 +1,37 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.model.response; + +import lombok.Data; + +/** + * Response class with temporary key. + * + * @author Petr Dvorak, petr@wultra.com + */ +@Data +public class TemporaryKeyResponse { + + /** + * JWT with encoded temporary key response. + */ + private String jwt; + +} diff --git a/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/encryption/EncryptionContext.java b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/encryption/EncryptionContext.java index c919dbea..ce60e171 100644 --- a/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/encryption/EncryptionContext.java +++ b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/encryption/EncryptionContext.java @@ -44,6 +44,7 @@ public class EncryptionContext { * Protocol version. */ private final String version; + /** * PowerAuth HTTP header used for deriving ECIES encryption context. */ diff --git a/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthTemporaryKeyException.java b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthTemporaryKeyException.java new file mode 100644 index 00000000..02db78a3 --- /dev/null +++ b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthTemporaryKeyException.java @@ -0,0 +1,55 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.exception; + +/** + * Exception raised in case PowerAuth fails to return temporary keys. + * + * @author Petr Dvorak, petr@wultra.com + */ +public class PowerAuthTemporaryKeyException extends Exception { + + private static final String DEFAULT_CODE = "ERR_TEMPORARY_KEY"; + private static final String DEFAULT_ERROR = "POWER_AUTH_TEMPORARY_KEY_FAILURE"; + + /** + * Default constructor. + */ + public PowerAuthTemporaryKeyException() { + super(DEFAULT_ERROR); + } + + /** + * Get the default error code, used for example in REST response. + * @return Default error code. + */ + public String getDefaultCode() { + return DEFAULT_CODE; + } + + /** + * Get default error message, used for example in the REST response. + * @return Default error message. + */ + public String getDefaultError() { + return DEFAULT_ERROR; + } + +} diff --git a/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/provider/PowerAuthEncryptionProvider.java b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/provider/PowerAuthEncryptionProvider.java index 53e05116..c887770e 100644 --- a/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/provider/PowerAuthEncryptionProvider.java +++ b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/provider/PowerAuthEncryptionProvider.java @@ -58,11 +58,12 @@ public PowerAuthEncryptionProvider(PowerAuthClient powerAuthClient, HttpCustomiz } @Override - public @Nonnull PowerAuthEncryptorParameters getEciesDecryptorParameters(@Nullable String activationId, @Nonnull String applicationKey, @Nonnull String ephemeralPublicKey, @Nonnull String version, String nonce, Long timestamp) throws PowerAuthEncryptionException { + public @Nonnull PowerAuthEncryptorParameters getEciesDecryptorParameters(@Nullable String activationId, @Nonnull String applicationKey, @Nonnull String temporaryKeyId, @Nonnull String ephemeralPublicKey, @Nonnull String version, String nonce, Long timestamp) throws PowerAuthEncryptionException { try { final GetEciesDecryptorRequest eciesDecryptorRequest = new GetEciesDecryptorRequest(); eciesDecryptorRequest.setActivationId(activationId); eciesDecryptorRequest.setApplicationKey(applicationKey); + eciesDecryptorRequest.setTemporaryKeyId(temporaryKeyId); eciesDecryptorRequest.setEphemeralPublicKey(ephemeralPublicKey); eciesDecryptorRequest.setProtocolVersion(version); eciesDecryptorRequest.setNonce(nonce); diff --git a/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/provider/PowerAuthEncryptionProviderBase.java b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/provider/PowerAuthEncryptionProviderBase.java index bab6972e..48a3b692 100644 --- a/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/provider/PowerAuthEncryptionProviderBase.java +++ b/powerauth-restful-security-spring-annotation/src/main/java/io/getlime/security/powerauth/rest/api/spring/provider/PowerAuthEncryptionProviderBase.java @@ -80,7 +80,7 @@ public abstract class PowerAuthEncryptionProviderBase { * @throws PowerAuthEncryptionException In case PowerAuth server call fails. */ public abstract @Nonnull - PowerAuthEncryptorParameters getEciesDecryptorParameters(@Nullable String activationId, @Nonnull String applicationKey, @Nonnull String ephemeralPublicKey, @Nonnull String version, String nonce, Long timestamp) throws PowerAuthEncryptionException; + PowerAuthEncryptorParameters getEciesDecryptorParameters(@Nullable String activationId, @Nonnull String applicationKey, @Nonnull String temporaryKeyId, @Nonnull String ephemeralPublicKey, @Nonnull String version, String nonce, Long timestamp) throws PowerAuthEncryptionException; /** * Decrypt HTTP request body and construct object with ECIES data. Use the requestType parameter to specify @@ -136,6 +136,7 @@ public void decryptRequest(@Nonnull HttpServletRequest request, @Nonnull Type re // Prepare and validate EncryptedRequest object final EncryptedRequest encryptedRequest = new EncryptedRequest( + eciesRequest.getTemporaryKeyId(), eciesRequest.getEphemeralPublicKey(), eciesRequest.getEncryptedData(), eciesRequest.getMac(), @@ -155,6 +156,7 @@ public void decryptRequest(@Nonnull HttpServletRequest request, @Nonnull Type re final PowerAuthEncryptorParameters encryptorParameters = getEciesDecryptorParameters( activationId, applicationKey, + encryptedRequest.getTemporaryKeyId(), encryptedRequest.getEphemeralPublicKey(), version, encryptedRequest.getNonce(), @@ -165,7 +167,7 @@ public void decryptRequest(@Nonnull HttpServletRequest request, @Nonnull Type re final byte[] sharedInfo2Base = Base64.getDecoder().decode(encryptorParameters.sharedInfo2()); final ServerEncryptor serverEncryptor = encryptorFactory.getServerEncryptor( encryptorData.getEncryptorId(), - new EncryptorParameters(version, applicationKey, activationId), + new EncryptorParameters(version, applicationKey, activationId, encryptedRequest.getTemporaryKeyId()), new ServerEncryptorSecrets(secretKeyBytes, sharedInfo2Base) ); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ActivationController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ActivationController.java index 72853684..e94847e4 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ActivationController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ActivationController.java @@ -60,6 +60,9 @@ *

PowerAuth protocol versions: *

    *
  • 3.0
  • + *
  • 3.1
  • + *
  • 3.2
  • + *
  • 3.3
  • *
* * @author Roman Strobl, roman.strobl@wultra.com diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/KeyStoreController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/KeyStoreController.java new file mode 100644 index 00000000..76a706b4 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/KeyStoreController.java @@ -0,0 +1,85 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.controller; + +import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.core.rest.model.base.response.ObjectResponse; +import io.getlime.security.powerauth.rest.api.model.request.TemporaryKeyRequest; +import io.getlime.security.powerauth.rest.api.model.response.TemporaryKeyResponse; +import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthTemporaryKeyException; +import io.getlime.security.powerauth.rest.api.spring.service.KeyStoreService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +/** + * Controller for obtaining temporary encryption keys. + * + *

PowerAuth protocol versions: + *

    + *
  • 3.3
  • + *
+ * + * @author Petr Dvorak, petr@wultra.com + */ +@RestController("keyStoreControllerV3") +@RequestMapping(value = "/pa/v3/keystore") +public class KeyStoreController { + + private static final Logger logger = LoggerFactory.getLogger(KeyStoreController.class); + + private final KeyStoreService service; + + /** + * Default autowiring constructor. + * @param service Keystore service. + */ + @Autowired + public KeyStoreController(KeyStoreService service) { + this.service = service; + } + + /** + * Create a new temporary key. + * @param request Request for temporary key. + * @return Response with temporary key. + * @throws PowerAuthTemporaryKeyException In case temporary key cannot be returned. + */ + @PostMapping("create") + public ObjectResponse fetchTemporaryKey(@RequestBody ObjectRequest request) throws PowerAuthTemporaryKeyException { + if (request == null) { + logger.warn("Null request while fetching temporary key"); + throw new PowerAuthTemporaryKeyException(); + } + final TemporaryKeyRequest requestObject = request.getRequestObject(); + if (requestObject == null) { + logger.warn("Null request object while fetching temporary key"); + throw new PowerAuthTemporaryKeyException(); + } + if (!StringUtils.hasLength(requestObject.getJwt())) { + logger.warn("Invalid request object with empty JWT while fetching temporary key"); + throw new PowerAuthTemporaryKeyException(); + } + return new ObjectResponse<>(service.fetchTemporaryKey(requestObject)); + } + +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/RecoveryController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/RecoveryController.java index fb208457..a4664b52 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/RecoveryController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/RecoveryController.java @@ -44,6 +44,9 @@ *

PowerAuth protocol versions: *

    *
  • 3.0
  • + *
  • 3.1
  • + *
  • 3.2
  • + *
  • 3.3
  • *
* * @author Roman Strobl, roman.strobl@wultra.com @@ -85,8 +88,7 @@ public EciesEncryptedResponse confirmRecoveryCode(@RequestBody EciesEncryptedReq PowerAuthAuthenticationUtil.checkAuthentication(auth); PowerAuthVersionUtil.checkUnsupportedVersion(auth.getVersion()); - PowerAuthVersionUtil.checkMissingRequiredNonce(auth.getVersion(), request.getNonce()); - PowerAuthVersionUtil.checkMissingRequiredTimestamp(auth.getVersion(), request.getTimestamp()); + PowerAuthVersionUtil.checkEciesParameters(auth.getVersion(), request); return recoveryService.confirmRecoveryCode(request, auth); } diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SecureVaultController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SecureVaultController.java index 72089db0..5c4c0100 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SecureVaultController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SecureVaultController.java @@ -44,6 +44,9 @@ *

PowerAuth protocol versions: *

    *
  • 3.0
  • + *
  • 3.1
  • + *
  • 3.2
  • + *
  • 3.3
  • *
* * @author Roman Strobl, roman.strobl@wultra.com @@ -100,9 +103,7 @@ public EciesEncryptedResponse unlockVault( } PowerAuthVersionUtil.checkUnsupportedVersion(header.getVersion()); - PowerAuthVersionUtil.checkMissingRequiredNonce(header.getVersion(), request.getNonce()); - PowerAuthVersionUtil.checkMissingRequiredTimestamp(header.getVersion(), request.getTimestamp()); - + PowerAuthVersionUtil.checkEciesParameters(header.getVersion(), request); return secureVaultServiceV3.vaultUnlock(header, request, httpServletRequest); } diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ServerStatusController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ServerStatusController.java index 9e422a91..7bc046f1 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ServerStatusController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ServerStatusController.java @@ -22,6 +22,8 @@ import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.security.powerauth.rest.api.model.response.ServerStatusResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.info.BuildProperties; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -35,6 +37,7 @@ *
  • 3.0
  • *
  • 3.1
  • *
  • 3.2
  • + *
  • 3.3
  • * * * @author Petr Dvorak, petr@wultra.com @@ -44,6 +47,13 @@ @Slf4j public class ServerStatusController { + private BuildProperties buildProperties; + + @Autowired(required = false) + public void setBuildProperties(BuildProperties buildProperties) { + this.buildProperties = buildProperties; + } + /** * Obtain server status. * @return Server status. @@ -51,7 +61,17 @@ public class ServerStatusController { @PostMapping("status") public ObjectResponse getServerStatus() { final long serverTime = new Date().getTime(); - final ServerStatusResponse response = new ServerStatusResponse(serverTime); + final String version; + final String name; + if (buildProperties != null) { + version = buildProperties.getVersion(); + name = buildProperties.getName(); + } else { + name = "UNKNOWN"; + version = "UNKNOWN"; + } + final ServerStatusResponse.Application application = new ServerStatusResponse.Application(name, version); + final ServerStatusResponse response = new ServerStatusResponse(serverTime, application); return new ObjectResponse<>(response); } diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SignatureController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SignatureController.java index a3f3b7a8..37b02a90 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SignatureController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/SignatureController.java @@ -36,6 +36,9 @@ *

    PowerAuth protocol versions: *

      *
    • 3.0
    • + *
    • 3.1
    • + *
    • 3.2
    • + *
    • 3.3
    • *
    * * @author Roman Strobl, roman.strobl@wultra.com diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/TokenController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/TokenController.java index 8498a691..3d492be8 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/TokenController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/TokenController.java @@ -47,6 +47,9 @@ *

    PowerAuth protocol versions: *

      *
    • 3.0
    • + *
    • 3.1
    • + *
    • 3.2
    • + *
    • 3.3
    • *
    * * @author Petr Dvorak, petr@wultra.com @@ -92,8 +95,7 @@ public EciesEncryptedResponse createToken(@RequestBody EciesEncryptedRequest req PowerAuthAuthenticationUtil.checkAuthentication(auth); PowerAuthVersionUtil.checkUnsupportedVersion(auth.getVersion()); - PowerAuthVersionUtil.checkMissingRequiredNonce(auth.getVersion(), request.getNonce()); - PowerAuthVersionUtil.checkMissingRequiredTimestamp(auth.getVersion(), request.getTimestamp()); + PowerAuthVersionUtil.checkEciesParameters(auth.getVersion(), request); return tokenServiceV3.createToken(request, auth); } diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/UpgradeController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/UpgradeController.java index 4fb94ea8..8c3105c8 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/UpgradeController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/UpgradeController.java @@ -46,6 +46,10 @@ *

    PowerAuth protocol versions: *

      *
    • 3.0
    • + *
    • 3.1
    • + *
    • 3.2
    • + *
    • 3.3
    • + * *
    * * @author Roman Strobl, roman.strobl@wultra @@ -98,8 +102,7 @@ public EciesEncryptedResponse upgradeStart(@RequestBody EciesEncryptedRequest re } PowerAuthVersionUtil.checkUnsupportedVersion(header.getVersion()); - PowerAuthVersionUtil.checkMissingRequiredNonce(header.getVersion(), request.getNonce()); - PowerAuthVersionUtil.checkMissingRequiredTimestamp(header.getVersion(), request.getTimestamp()); + PowerAuthVersionUtil.checkEciesParameters(header.getVersion(), request); return upgradeService.upgradeStart(request, header); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/UserInfoController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/UserInfoController.java index f922524b..965da52a 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/UserInfoController.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/UserInfoController.java @@ -42,6 +42,7 @@ *
  • 3.0
  • *
  • 3.1
  • *
  • 3.2
  • + *
  • 3.3
  • * * * @author Petr Dvorak, petr@wultra.com diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthExceptionHandler.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthExceptionHandler.java index 20635bd2..cbc99f6f 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthExceptionHandler.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthExceptionHandler.java @@ -132,4 +132,16 @@ public class PowerAuthExceptionHandler { return new ErrorResponse(ex.getDefaultCode(), ex.getMessage()); } + /** + * Handle PowerAuthTemporaryKeyException exceptions. + * @param ex Exception instance. + * @return Error response. + */ + @ExceptionHandler(value = PowerAuthTemporaryKeyException.class) + @ResponseStatus(value = HttpStatus.BAD_REQUEST) + public @ResponseBody ErrorResponse handlePowerAuthTemporaryKeyException(PowerAuthTemporaryKeyException ex) { + logger.warn(ex.getMessage(), ex); + return new ErrorResponse(ex.getDefaultCode(), ex.getDefaultError()); + } + } diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java index 48dffc89..24b8df53 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java @@ -134,6 +134,7 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request try { final String applicationKey = eciesContext.getApplicationKey(); final EciesEncryptedRequest activationData = request.getActivationData(); + final String temporaryKeyId = activationData.getTemporaryKeyId(); final String ephemeralPublicKey = activationData.getEphemeralPublicKey(); final String encryptedData = activationData.getEncryptedData(); final String mac = activationData.getMac(); @@ -161,6 +162,7 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request prepareRequest.setActivationCode(activationCode); prepareRequest.setApplicationKey(applicationKey); prepareRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes(identity, customAttributes, context)); + prepareRequest.setTemporaryKeyId(temporaryKeyId); prepareRequest.setEphemeralPublicKey(ephemeralPublicKey); prepareRequest.setEncryptedData(encryptedData); prepareRequest.setMac(mac); @@ -265,6 +267,7 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request createRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes); createRequest.setMaxFailureCount(maxFailedCount); createRequest.setApplicationKey(applicationKey); + createRequest.setTemporaryKeyId(temporaryKeyId); createRequest.setEphemeralPublicKey(ephemeralPublicKey); createRequest.setEncryptedData(encryptedData); createRequest.setMac(mac); @@ -354,6 +357,7 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request recoveryRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes); recoveryRequest.setApplicationKey(applicationKey); recoveryRequest.setMaxFailureCount(maxFailedCount); + recoveryRequest.setTemporaryKeyId(temporaryKeyId); recoveryRequest.setEphemeralPublicKey(ephemeralPublicKey); recoveryRequest.setEncryptedData(encryptedData); recoveryRequest.setMac(mac); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/KeyStoreService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/KeyStoreService.java new file mode 100644 index 00000000..a0db61a8 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/KeyStoreService.java @@ -0,0 +1,84 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service; + +import com.wultra.security.powerauth.client.PowerAuthClient; +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.client.model.request.TemporaryPublicKeyRequest; +import com.wultra.security.powerauth.client.model.response.TemporaryPublicKeyResponse; +import io.getlime.security.powerauth.rest.api.model.request.TemporaryKeyRequest; +import io.getlime.security.powerauth.rest.api.model.response.TemporaryKeyResponse; +import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthTemporaryKeyException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Key store service for obtaining temporary encryption keys. + * + * @author Petr Dvorak, petr@wultra.com + */ +@Service +public class KeyStoreService { + + private static final Logger logger = LoggerFactory.getLogger(KeyStoreService.class); + + private final PowerAuthClient powerAuthClient; + private final HttpCustomizationService httpCustomizationService; + + /** + * Default autowiring constructor. + * @param powerAuthClient PowerAuth Client + * @param httpCustomizationService Customization service. + */ + @Autowired + public KeyStoreService(PowerAuthClient powerAuthClient, HttpCustomizationService httpCustomizationService) { + this.powerAuthClient = powerAuthClient; + this.httpCustomizationService = httpCustomizationService; + } + + /** + * Fetch a temporary public key with provided parameters. + * @param request Temporary public key request. + * @return Response with temporary public key. + * @throws PowerAuthTemporaryKeyException In case internal API call fails. + */ + public TemporaryKeyResponse fetchTemporaryKey(TemporaryKeyRequest request) throws PowerAuthTemporaryKeyException { + try { + final TemporaryPublicKeyRequest publicKeyRequest = new TemporaryPublicKeyRequest(); + publicKeyRequest.setJwt(request.getJwt()); + + final TemporaryPublicKeyResponse temporaryPublicKeyResponse = powerAuthClient.fetchTemporaryPublicKey( + publicKeyRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + + final TemporaryKeyResponse response = new TemporaryKeyResponse(); + response.setJwt(temporaryPublicKeyResponse.getJwt()); + return response; + } catch (PowerAuthClientException ex) { + logger.warn("PowerAuth fetching temporary key failed, error: {}", ex.getMessage()); + logger.debug(ex.getMessage(), ex); + throw new PowerAuthTemporaryKeyException(); + } + } +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/RecoveryService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/RecoveryService.java index 0ea79a6f..a8ede082 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/RecoveryService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/RecoveryService.java @@ -83,6 +83,7 @@ public EciesEncryptedResponse confirmRecoveryCode(EciesEncryptedRequest request, final ConfirmRecoveryCodeRequest confirmRequest = new ConfirmRecoveryCodeRequest(); confirmRequest.setActivationId(activationId); confirmRequest.setApplicationKey(applicationKey); + confirmRequest.setTemporaryKeyId(request.getTemporaryKeyId()); confirmRequest.setEphemeralPublicKey(request.getEphemeralPublicKey()); confirmRequest.setEncryptedData(request.getEncryptedData()); confirmRequest.setMac(request.getMac()); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/SecureVaultService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/SecureVaultService.java index f0304ba6..98ed816b 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/SecureVaultService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/SecureVaultService.java @@ -112,6 +112,7 @@ public EciesEncryptedResponse vaultUnlock(PowerAuthSignatureHttpHeader header, unlockRequest.setSignatureType(signatureType); unlockRequest.setSignatureVersion(signatureVersion); unlockRequest.setSignedData(data); + unlockRequest.setTemporaryKeyId(request.getTemporaryKeyId()); unlockRequest.setEphemeralPublicKey(request.getEphemeralPublicKey()); unlockRequest.setEncryptedData(request.getEncryptedData()); unlockRequest.setMac(request.getMac()); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/TokenService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/TokenService.java index 56312032..cd409ff7 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/TokenService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/TokenService.java @@ -101,6 +101,7 @@ public EciesEncryptedResponse createToken(EciesEncryptedRequest request, final CreateTokenRequest tokenRequest = new CreateTokenRequest(); tokenRequest.setActivationId(activationId); tokenRequest.setApplicationKey(applicationKey); + tokenRequest.setTemporaryKeyId(request.getTemporaryKeyId()); tokenRequest.setEphemeralPublicKey(request.getEphemeralPublicKey()); tokenRequest.setEncryptedData(request.getEncryptedData()); tokenRequest.setMac(request.getMac()); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/UpgradeService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/UpgradeService.java index c8bfa948..2e34f45f 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/UpgradeService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/UpgradeService.java @@ -97,6 +97,7 @@ public EciesEncryptedResponse upgradeStart(EciesEncryptedRequest request, PowerA final StartUpgradeRequest upgradeRequest = new StartUpgradeRequest(); upgradeRequest.setActivationId(activationId); upgradeRequest.setApplicationKey(applicationKey); + upgradeRequest.setTemporaryKeyId(request.getTemporaryKeyId()); upgradeRequest.setEphemeralPublicKey(request.getEphemeralPublicKey()); upgradeRequest.setEncryptedData(request.getEncryptedData()); upgradeRequest.setMac(request.getMac()); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/util/PowerAuthVersionUtil.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/util/PowerAuthVersionUtil.java index cad9ecf7..9537df46 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/util/PowerAuthVersionUtil.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/util/PowerAuthVersionUtil.java @@ -19,6 +19,7 @@ */ package io.getlime.security.powerauth.rest.api.spring.util; +import io.getlime.security.powerauth.rest.api.model.request.EciesEncryptedRequest; import io.getlime.security.powerauth.rest.api.spring.exception.authentication.PowerAuthInvalidRequestException; import lombok.extern.slf4j.Slf4j; @@ -47,7 +48,7 @@ private PowerAuthVersionUtil() { /** * Set containing all the supported versions of PowerAuth. */ - private static final Set SUPPORTED_VERSIONS = Set.of("3.0", "3.1", "3.2"); + private static final Set SUPPORTED_VERSIONS = Set.of("3.0", "3.1", "3.2", "3.3"); /** * Check if the provided version string is "3.0". @@ -69,6 +70,16 @@ private static boolean isVersion3_1(final String version) { return "3.1".equals(version); } + /** + * Check if the provided version string is "3.2". + * + * @param version Version string to be checked. + * @return true if the version is "3.2", false otherwise. + */ + private static boolean isVersion3_2(final String version) { + return "3.2".equals(version); + } + /** * Checks if the provided PowerAuth protocol version is unsupported. * Throws an exception if the version is unsupported. @@ -113,6 +124,35 @@ public static void checkMissingRequiredTimestamp(String version, Long timestamp) } } + /** + * Checks if temporary key ID is missing for the provided PowerAuth protocol version. + * Throws an exception if the temporary key ID is required and missing. + * + * @param version Version string to be checked. + * @param temporaryKeyId Temporary key ID value to be verified. + * @throws PowerAuthInvalidRequestException If timestamp is required and missing. + */ + public static void checkMissingRequiredTemporaryKeyId(String version, String temporaryKeyId) throws PowerAuthInvalidRequestException { + if (isMissingRequiredTemporaryKeyId(version, temporaryKeyId)) { + logger.warn("Missing temporary key ID in ECIES request data for version {}", version); + throw new PowerAuthInvalidRequestException("Missing temporary kdy ID in ECIES request data for version " + version); + } + } + + /** + * Checks if required ECIES parameters are missing for the provided PowerAuth protocol version. + * Throws an exception if the required parameter is missing. + * + * @param version Version string to be checked. + * @param request Request to be verified. + * @throws PowerAuthInvalidRequestException If timestamp is required and missing. + */ + public static void checkEciesParameters(String version, EciesEncryptedRequest request) throws PowerAuthInvalidRequestException { + checkMissingRequiredNonce(version, request.getNonce()); + checkMissingRequiredTimestamp(version, request.getTimestamp()); + checkMissingRequiredTemporaryKeyId(version, request.getTemporaryKeyId()); + } + /** * Checks if the provided PowerAuth protocol version is unsupported. * @@ -146,4 +186,19 @@ private static boolean isMissingRequiredTimestamp(String version, Long timestamp !isVersion3_0(version) && !isVersion3_1(version); } + + /** + * Checks if temporary key ID is missing for the provided PowerAuth protocol version. + * + * @param version Version string to be checked. + * @param temporaryKeyId Temporary key ID + * @return true if temporary key ID is required and missing, false otherwise. + */ + private static boolean isMissingRequiredTemporaryKeyId(String version, String temporaryKeyId) { + return temporaryKeyId == null && + !isVersion3_0(version) && + !isVersion3_1(version) && + !isVersion3_2(version); + } + } diff --git a/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/PowerAuthVersionUtilTest.java b/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/PowerAuthVersionUtilTest.java index 6d15f81a..73b67fb3 100644 --- a/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/PowerAuthVersionUtilTest.java +++ b/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/PowerAuthVersionUtilTest.java @@ -23,6 +23,8 @@ import io.getlime.security.powerauth.rest.api.spring.util.PowerAuthVersionUtil; import org.junit.jupiter.api.Test; +import java.util.UUID; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -62,4 +64,12 @@ void testMissingRequiredTimestamp() { assertDoesNotThrow(() -> PowerAuthVersionUtil.checkMissingRequiredTimestamp("3.1", null)); assertDoesNotThrow(() -> PowerAuthVersionUtil.checkMissingRequiredTimestamp("3.2", 1630234567890L)); } + + @Test + void testMissingRequiredTemporaryKeyId() { + assertThrows(PowerAuthInvalidRequestException.class, () -> PowerAuthVersionUtil.checkMissingRequiredTemporaryKeyId("3.3", null)); + assertDoesNotThrow(() -> PowerAuthVersionUtil.checkMissingRequiredTemporaryKeyId("3.1", null)); + assertDoesNotThrow(() -> PowerAuthVersionUtil.checkMissingRequiredTemporaryKeyId("3.2", null)); + assertDoesNotThrow(() -> PowerAuthVersionUtil.checkMissingRequiredTemporaryKeyId("3.3", UUID.randomUUID().toString())); + } } \ No newline at end of file From 733c412ae40054bfa7fa0efe63acb84447ce0829 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 05:13:03 +0000 Subject: [PATCH 08/19] Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.8.0 to 3.10.0 Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.8.0 to 3.10.0. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.8.0...maven-javadoc-plugin-3.10.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 199b5adc..e6a92cc0 100644 --- a/pom.xml +++ b/pom.xml @@ -81,7 +81,7 @@ 3.1.3 3.5.0 3.4.2 - 3.8.0 + 3.10.0 3.3.1 3.4.0 3.3.3 From 10c03d778b54e6e63a5a95bd3d13b970b9ceadac Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Fri, 23 Aug 2024 09:24:42 +0200 Subject: [PATCH 09/19] Fix #548: Simplify ActivationService#createActivation --- .../api/spring/service/ActivationService.java | 581 +++++++++--------- 1 file changed, 293 insertions(+), 288 deletions(-) diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java index 24b8df53..e20748ba 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java @@ -38,13 +38,11 @@ import io.getlime.security.powerauth.rest.api.spring.encryption.EncryptionContext; import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthActivationException; import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthRecoveryException; -import io.getlime.security.powerauth.rest.api.spring.exception.authentication.PowerAuthInvalidRequestException; import io.getlime.security.powerauth.rest.api.spring.model.ActivationContext; import io.getlime.security.powerauth.rest.api.spring.model.UserInfoContext; import io.getlime.security.powerauth.rest.api.spring.provider.CustomActivationProvider; import io.getlime.security.powerauth.rest.api.spring.provider.UserInfoProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -64,10 +62,9 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Service("activationServiceV3") +@Slf4j public class ActivationService { - private static final Logger logger = LoggerFactory.getLogger(ActivationService.class); - private final PowerAuthClient powerAuthClient; private final HttpCustomizationService httpCustomizationService; private final ActivationContextConverter activationContextConverter; @@ -127,293 +124,19 @@ public void setUserInfoProvider(UserInfoProvider userInfoProvider) { * @throws PowerAuthRecoveryException In case activation recovery fails. */ public ActivationLayer1Response createActivation(ActivationLayer1Request request, EncryptionContext eciesContext) throws PowerAuthActivationException, PowerAuthRecoveryException { + final ActivationType type = request.getType(); + logger.debug("Handling activation type: {}", type); final Map identity = request.getIdentityAttributes(); checkIdentityAttributesPresent(identity); try { - final String applicationKey = eciesContext.getApplicationKey(); - final EciesEncryptedRequest activationData = request.getActivationData(); - final String temporaryKeyId = activationData.getTemporaryKeyId(); - final String ephemeralPublicKey = activationData.getEphemeralPublicKey(); - final String encryptedData = activationData.getEncryptedData(); - final String mac = activationData.getMac(); - final String nonce = activationData.getNonce(); - final Long timestamp = activationData.getTimestamp(); - final Map customAttributes = (request.getCustomAttributes() != null) ? request.getCustomAttributes() : new HashMap<>(); - - switch (request.getType()) { + return switch (type) { // Regular activation which uses "code" identity attribute - case CODE -> { - - // Extract data from request and encryption object - final String activationCode = identity.get("code"); - - if (!StringUtils.hasText(activationCode)) { - logger.warn("Activation code is missing"); - throw new PowerAuthActivationException(); - } - - // Create context for passing parameters between activation provider calls - final Map context = new LinkedHashMap<>(); - - // Call PrepareActivation method on PA server - final PrepareActivationRequest prepareRequest = new PrepareActivationRequest(); - prepareRequest.setActivationCode(activationCode); - prepareRequest.setApplicationKey(applicationKey); - prepareRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes(identity, customAttributes, context)); - prepareRequest.setTemporaryKeyId(temporaryKeyId); - prepareRequest.setEphemeralPublicKey(ephemeralPublicKey); - prepareRequest.setEncryptedData(encryptedData); - prepareRequest.setMac(mac); - prepareRequest.setNonce(nonce); - prepareRequest.setProtocolVersion(eciesContext.getVersion()); - prepareRequest.setTimestamp(timestamp); - final PrepareActivationResponse response = powerAuthClient.prepareActivation( - prepareRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - - final String userId = response.getUserId(); - final String activationId = response.getActivationId(); - final String applicationId = response.getApplicationId(); - - final UserInfoContext userInfoContext = UserInfoContext.builder() - .stage(UserInfoStage.ACTIVATION_PROCESS_ACTIVATION_CODE) - .userId(userId) - .activationId(activationId) - .applicationId(applicationId) - .build(); - final Map userInfo = processUserInfo(userInfoContext); - - Map processedCustomAttributes = customAttributes; - // In case a custom activation provider is enabled, process custom attributes and save any flags - if (activationProvider != null) { - processedCustomAttributes = activationProvider.processCustomActivationAttributes(customAttributes, activationId, userId, applicationId, ActivationType.CODE, context); - final List activationFlags = activationProvider.getActivationFlags(identity, processedCustomAttributes, activationId, userId, applicationId, ActivationType.CODE, context); - if (activationFlags != null && !activationFlags.isEmpty()) { - final AddActivationFlagsRequest flagsRequest = new AddActivationFlagsRequest(); - flagsRequest.setActivationId(activationId); - flagsRequest.getActivationFlags().addAll(activationFlags); - powerAuthClient.addActivationFlags( - flagsRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - } - } - - boolean notifyActivationCommit = false; - if (response.getActivationStatus() == ActivationStatus.ACTIVE) { - // Activation was committed instantly due to presence of Activation OTP. - notifyActivationCommit = true; - } else { - // Otherwise check if activation should be committed instantly and if yes, perform commit. - if (activationProvider != null && activationProvider.shouldAutoCommitActivation(identity, customAttributes, activationId, userId, applicationId, ActivationType.CODE, context)) { - final CommitActivationResponse commitResponse = commitActivation(activationId); - notifyActivationCommit = commitResponse.isActivated(); - } - } - // Notify activation provider about an activation commit. - if (activationProvider != null && notifyActivationCommit) { - activationProvider.activationWasCommitted(identity, customAttributes, activationId, userId, applicationId, ActivationType.CODE, context); - } - - // Prepare and return encrypted response - return prepareEncryptedResponse(response.getEncryptedData(), response.getMac(), - response.getNonce(), response.getTimestamp(), processedCustomAttributes, userInfo); - } - - - // Custom activation - case CUSTOM -> { - // Check if there is a custom activation provider available, return an error in case it is not available. - // Only for CUSTOM activations, proceeding without an activation provider does not make a sensible use-case. - if (activationProvider == null) { - logger.warn("Activation provider is not available"); - throw new PowerAuthActivationException(); - } - - // Create context for passing parameters between activation provider calls - final Map context = new LinkedHashMap<>(); - - // Lookup user ID using a provided identity attributes - final String userId = activationProvider.lookupUserIdForAttributes(identity, context); - - // If no user was found or user ID is invalid, return an error - if (!StringUtils.hasText(userId) || userId.length() > 255) { - logger.warn("Invalid user ID: {}", userId); - throw new PowerAuthActivationException(); - } - - // Decide if the recovery codes should be generated - final boolean shouldGenerateRecoveryCodes = activationProvider.shouldCreateRecoveryCodes(identity, customAttributes, ActivationType.CODE, context); - - // Resolve maxFailedCount and activationExpireTimestamp parameters, null value means use value configured on PowerAuth server - final Integer maxFailed = activationProvider.getMaxFailedAttemptCount(identity, customAttributes, userId, ActivationType.CUSTOM, context); - final Long maxFailedCount = maxFailed == null ? null : maxFailed.longValue(); - final Long activationValidityPeriod = activationProvider.getValidityPeriodDuringActivation(identity, customAttributes, userId, ActivationType.CUSTOM, context); - Date activationExpire = null; - if (activationValidityPeriod != null) { - final Instant expiration = Instant.now().plusMillis(activationValidityPeriod); - activationExpire = Date.from(expiration); - } - - // Create activation for a looked up user and application related to the given application key - final CreateActivationRequest createRequest = new CreateActivationRequest(); - createRequest.setUserId(userId); - createRequest.setTimestampActivationExpire(activationExpire); - createRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes); - createRequest.setMaxFailureCount(maxFailedCount); - createRequest.setApplicationKey(applicationKey); - createRequest.setTemporaryKeyId(temporaryKeyId); - createRequest.setEphemeralPublicKey(ephemeralPublicKey); - createRequest.setEncryptedData(encryptedData); - createRequest.setMac(mac); - createRequest.setNonce(nonce); - createRequest.setProtocolVersion(eciesContext.getVersion()); - createRequest.setTimestamp(timestamp); - final CreateActivationResponse response = powerAuthClient.createActivation( - createRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - - final String activationId = response.getActivationId(); - final String applicationId = response.getApplicationId(); - - final UserInfoContext userInfoContext = UserInfoContext.builder() - .stage(UserInfoStage.ACTIVATION_PROCESS_CUSTOM) - .userId(userId) - .activationId(activationId) - .applicationId(applicationId) - .build(); - final Map userInfo = processUserInfo(userInfoContext); - - // Process custom attributes using a custom logic - final Map processedCustomAttributes = activationProvider.processCustomActivationAttributes(customAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context); - - // Save activation flags in case the provider specified any flags - final List activationFlags = activationProvider.getActivationFlags(identity, processedCustomAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context); - if (activationFlags != null && !activationFlags.isEmpty()) { - final AddActivationFlagsRequest flagsRequest = new AddActivationFlagsRequest(); - flagsRequest.setActivationId(activationId); - flagsRequest.getActivationFlags().addAll(activationFlags); - powerAuthClient.addActivationFlags( - flagsRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - } - - // Check if activation should be committed instantly and if yes, perform commit - if (activationProvider.shouldAutoCommitActivation(identity, customAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context)) { - final CommitActivationResponse commitResponse = commitActivation(activationId); - if (commitResponse.isActivated()) { - activationProvider.activationWasCommitted(identity, customAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context); - } - } - - // Prepare encrypted activation data - return prepareEncryptedResponse(response.getEncryptedData(), response.getMac(), - response.getNonce(), response.getTimestamp(), processedCustomAttributes, userInfo); - } - - - // Activation using recovery code - case RECOVERY -> { - - // Extract data from request and encryption object - final String recoveryCode = identity.get("recoveryCode"); - final String recoveryPuk = identity.get("puk"); - - if (!StringUtils.hasText(recoveryCode)) { - logger.warn("Recovery code is missing"); - throw new PowerAuthActivationException(); - } - - if (!StringUtils.hasText(recoveryPuk)) { - logger.warn("Recovery PUK is missing"); - throw new PowerAuthActivationException(); - } - - // Create context for passing parameters between activation provider calls - final Map context = new LinkedHashMap<>(); - - // Resolve maxFailedCount, user ID is not known and decide if the recovery codes should be generated. - Long maxFailedCount = null; - Boolean shouldGenerateRecoveryCodes = null; - if (activationProvider != null) { - final Integer maxFailed = activationProvider.getMaxFailedAttemptCount(identity, customAttributes, null, ActivationType.RECOVERY, context); - maxFailedCount = maxFailed == null ? null : maxFailed.longValue(); - shouldGenerateRecoveryCodes = activationProvider.shouldCreateRecoveryCodes(identity, customAttributes, ActivationType.CODE, context); - } - - // Call RecoveryCodeActivation method on PA server - final RecoveryCodeActivationRequest recoveryRequest = new RecoveryCodeActivationRequest(); - recoveryRequest.setRecoveryCode(recoveryCode); - recoveryRequest.setPuk(recoveryPuk); - recoveryRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes); - recoveryRequest.setApplicationKey(applicationKey); - recoveryRequest.setMaxFailureCount(maxFailedCount); - recoveryRequest.setTemporaryKeyId(temporaryKeyId); - recoveryRequest.setEphemeralPublicKey(ephemeralPublicKey); - recoveryRequest.setEncryptedData(encryptedData); - recoveryRequest.setMac(mac); - recoveryRequest.setNonce(nonce); - recoveryRequest.setProtocolVersion(eciesContext.getVersion()); - recoveryRequest.setTimestamp(timestamp); - final RecoveryCodeActivationResponse response = powerAuthClient.createActivationUsingRecoveryCode( - recoveryRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - - final String userId = response.getUserId(); - final String activationId = response.getActivationId(); - final String applicationId = response.getApplicationId(); - - final UserInfoContext userInfoContext = UserInfoContext.builder() - .stage(UserInfoStage.ACTIVATION_PROCESS_RECOVERY) - .userId(userId) - .activationId(activationId) - .applicationId(applicationId) - .build(); - final Map userInfo = processUserInfo(userInfoContext); - - Map processedCustomAttributes = customAttributes; - // In case a custom activation provider is enabled, process custom attributes and save any flags - if (activationProvider != null) { - processedCustomAttributes = activationProvider.processCustomActivationAttributes(customAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context); - final List activationFlags = activationProvider.getActivationFlags(identity, processedCustomAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context); - if (activationFlags != null && !activationFlags.isEmpty()) { - final AddActivationFlagsRequest flagsRequest = new AddActivationFlagsRequest(); - flagsRequest.setActivationId(activationId); - flagsRequest.getActivationFlags().addAll(activationFlags); - powerAuthClient.addActivationFlags( - flagsRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - } - } - - // Automatically commit activation by default, the optional activation provider can override automatic commit - if (activationProvider == null || activationProvider.shouldAutoCommitActivation(identity, customAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context)) { - final CommitActivationResponse commitResponse = commitActivation(activationId); - if (activationProvider != null && commitResponse.isActivated()) { - activationProvider.activationWasCommitted(identity, customAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context); - } - } - - // Prepare and return encrypted response - return prepareEncryptedResponse(response.getEncryptedData(), response.getMac(), - response.getNonce(), response.getTimestamp(), processedCustomAttributes, userInfo); - } - default -> - throw new PowerAuthInvalidRequestException("Unsupported activation type: " + request.getType()); - } + case CODE -> processCodeActivation(eciesContext, request); + case CUSTOM -> processCustomActivation(eciesContext, request); + case RECOVERY -> processRecoveryCodeActivation(eciesContext, request); + }; } catch (PowerAuthClientException ex) { if (ex.getPowerAuthError().orElse(null) instanceof final PowerAuthErrorRecovery errorRecovery) { logger.debug("Invalid recovery code, current PUK index: {}", errorRecovery.getCurrentRecoveryPukIndex()); @@ -425,6 +148,288 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request } } + private ActivationLayer1Response processCodeActivation(final EncryptionContext eciesContext, final ActivationLayer1Request request) throws PowerAuthActivationException, PowerAuthClientException { + logger.debug("Processing recovery code activation."); + + final Map identity = request.getIdentityAttributes(); + + // Extract data from request and encryption object + final String activationCode = identity.get("code"); + + if (!StringUtils.hasText(activationCode)) { + throw new PowerAuthActivationException("Activation code is missing"); + } + + // Create context for passing parameters between activation provider calls + final Map context = new LinkedHashMap<>(); + + final Map customAttributes = Objects.requireNonNullElse(request.getCustomAttributes(), new HashMap<>()); + + final EciesEncryptedRequest activationData = request.getActivationData(); + + // Call PrepareActivation method on PA server + final PrepareActivationRequest prepareRequest = new PrepareActivationRequest(); + prepareRequest.setActivationCode(activationCode); + prepareRequest.setApplicationKey(eciesContext.getApplicationKey()); + prepareRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes(identity, customAttributes, context)); + prepareRequest.setTemporaryKeyId(activationData.getTemporaryKeyId()); + prepareRequest.setEphemeralPublicKey(activationData.getEphemeralPublicKey()); + prepareRequest.setEncryptedData(activationData.getEncryptedData()); + prepareRequest.setMac(activationData.getMac()); + prepareRequest.setNonce(activationData.getNonce()); + prepareRequest.setProtocolVersion(eciesContext.getVersion()); + prepareRequest.setTimestamp(activationData.getTimestamp()); + + final PrepareActivationResponse response = powerAuthClient.prepareActivation( + prepareRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + + final String userId = response.getUserId(); + final String activationId = response.getActivationId(); + final String applicationId = response.getApplicationId(); + + final UserInfoContext userInfoContext = UserInfoContext.builder() + .stage(UserInfoStage.ACTIVATION_PROCESS_ACTIVATION_CODE) + .userId(userId) + .activationId(activationId) + .applicationId(applicationId) + .build(); + final Map userInfo = processUserInfo(userInfoContext); + + Map processedCustomAttributes = customAttributes; + // In case a custom activation provider is enabled, process custom attributes and save any flags + if (activationProvider != null) { + processedCustomAttributes = activationProvider.processCustomActivationAttributes(customAttributes, activationId, userId, applicationId, ActivationType.CODE, context); + final List activationFlags = activationProvider.getActivationFlags(identity, processedCustomAttributes, activationId, userId, applicationId, ActivationType.CODE, context); + if (activationFlags != null && !activationFlags.isEmpty()) { + final AddActivationFlagsRequest flagsRequest = new AddActivationFlagsRequest(); + flagsRequest.setActivationId(activationId); + flagsRequest.getActivationFlags().addAll(activationFlags); + powerAuthClient.addActivationFlags( + flagsRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + } + } + + boolean notifyActivationCommit = false; + if (response.getActivationStatus() == ActivationStatus.ACTIVE) { + // Activation was committed instantly due to presence of Activation OTP. + notifyActivationCommit = true; + } else { + // Otherwise check if activation should be committed instantly and if yes, perform commit. + if (activationProvider != null && activationProvider.shouldAutoCommitActivation(identity, customAttributes, activationId, userId, applicationId, ActivationType.CODE, context)) { + final CommitActivationResponse commitResponse = commitActivation(activationId); + notifyActivationCommit = commitResponse.isActivated(); + } + } + // Notify activation provider about an activation commit. + if (activationProvider != null && notifyActivationCommit) { + activationProvider.activationWasCommitted(identity, customAttributes, activationId, userId, applicationId, ActivationType.CODE, context); + } + + // Prepare and return encrypted response + return prepareEncryptedResponse(response.getEncryptedData(), response.getMac(), + response.getNonce(), response.getTimestamp(), processedCustomAttributes, userInfo); + } + + private ActivationLayer1Response processRecoveryCodeActivation(final EncryptionContext eciesContext, final ActivationLayer1Request request) throws PowerAuthActivationException, PowerAuthClientException { + logger.debug("Processing recovery code activation."); + + final Map identity = request.getIdentityAttributes(); + + // Extract data from request and encryption object + final String recoveryCode = identity.get("recoveryCode"); + final String recoveryPuk = identity.get("puk"); + + if (!StringUtils.hasText(recoveryCode)) { + throw new PowerAuthActivationException("Recovery code is missing"); + } + + if (!StringUtils.hasText(recoveryPuk)) { + throw new PowerAuthActivationException("Recovery PUK is missing"); + } + + // Create context for passing parameters between activation provider calls + final Map context = new LinkedHashMap<>(); + + final Map customAttributes = Objects.requireNonNullElse(request.getCustomAttributes(), new HashMap<>()); + + // Resolve maxFailedCount, user ID is not known and decide if the recovery codes should be generated. + Long maxFailedCount = null; + Boolean shouldGenerateRecoveryCodes = null; + if (activationProvider != null) { + final Integer maxFailed = activationProvider.getMaxFailedAttemptCount(identity, customAttributes, null, ActivationType.RECOVERY, context); + maxFailedCount = maxFailed == null ? null : maxFailed.longValue(); + shouldGenerateRecoveryCodes = activationProvider.shouldCreateRecoveryCodes(identity, customAttributes, ActivationType.CODE, context); + } + + final EciesEncryptedRequest activationData = request.getActivationData(); + + // Call RecoveryCodeActivation method on PA server + final RecoveryCodeActivationRequest recoveryRequest = new RecoveryCodeActivationRequest(); + recoveryRequest.setRecoveryCode(recoveryCode); + recoveryRequest.setPuk(recoveryPuk); + recoveryRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes); + recoveryRequest.setApplicationKey(eciesContext.getApplicationKey()); + recoveryRequest.setMaxFailureCount(maxFailedCount); + recoveryRequest.setTemporaryKeyId(activationData.getTemporaryKeyId()); + recoveryRequest.setEphemeralPublicKey(activationData.getEphemeralPublicKey()); + recoveryRequest.setEncryptedData(activationData.getEncryptedData()); + recoveryRequest.setMac(activationData.getMac()); + recoveryRequest.setNonce(activationData.getNonce()); + recoveryRequest.setProtocolVersion(eciesContext.getVersion()); + recoveryRequest.setTimestamp(activationData.getTimestamp()); + + final RecoveryCodeActivationResponse response = powerAuthClient.createActivationUsingRecoveryCode( + recoveryRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + + final String userId = response.getUserId(); + final String activationId = response.getActivationId(); + final String applicationId = response.getApplicationId(); + + final UserInfoContext userInfoContext = UserInfoContext.builder() + .stage(UserInfoStage.ACTIVATION_PROCESS_RECOVERY) + .userId(userId) + .activationId(activationId) + .applicationId(applicationId) + .build(); + final Map userInfo = processUserInfo(userInfoContext); + + Map processedCustomAttributes = customAttributes; + // In case a custom activation provider is enabled, process custom attributes and save any flags + if (activationProvider != null) { + processedCustomAttributes = activationProvider.processCustomActivationAttributes(customAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context); + final List activationFlags = activationProvider.getActivationFlags(identity, processedCustomAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context); + if (activationFlags != null && !activationFlags.isEmpty()) { + final AddActivationFlagsRequest flagsRequest = new AddActivationFlagsRequest(); + flagsRequest.setActivationId(activationId); + flagsRequest.getActivationFlags().addAll(activationFlags); + powerAuthClient.addActivationFlags( + flagsRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + } + } + + // Automatically commit activation by default, the optional activation provider can override automatic commit + if (activationProvider == null || activationProvider.shouldAutoCommitActivation(identity, customAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context)) { + final CommitActivationResponse commitResponse = commitActivation(activationId); + if (activationProvider != null && commitResponse.isActivated()) { + activationProvider.activationWasCommitted(identity, customAttributes, activationId, userId, applicationId, ActivationType.RECOVERY, context); + } + } + + // Prepare and return encrypted response + return prepareEncryptedResponse(response.getEncryptedData(), response.getMac(), + response.getNonce(), response.getTimestamp(), processedCustomAttributes, userInfo); + } + + private ActivationLayer1Response processCustomActivation(final EncryptionContext eciesContext, final ActivationLayer1Request request) throws PowerAuthActivationException, PowerAuthClientException { + logger.debug("Processing custom activation."); + + if (activationProvider == null) { + throw new PowerAuthActivationException("Activation provider is not available"); + } + + // Create context for passing parameters between activation provider calls + final Map context = new LinkedHashMap<>(); + + final Map identity = request.getIdentityAttributes(); + + // Lookup user ID using a provided identity attributes + final String userId = activationProvider.lookupUserIdForAttributes(identity, context); + + // If no user was found or user ID is invalid, return an error + if (!StringUtils.hasText(userId) || userId.length() > 255) { + logger.warn("Invalid user ID: {}", userId); + throw new PowerAuthActivationException(); + } + + final Map customAttributes = Objects.requireNonNullElse(request.getCustomAttributes(), new HashMap<>()); + + // Decide if the recovery codes should be generated + final boolean shouldGenerateRecoveryCodes = activationProvider.shouldCreateRecoveryCodes(identity, customAttributes, ActivationType.CODE, context); + + // Resolve maxFailedCount and activationExpireTimestamp parameters, null value means use value configured on PowerAuth server + final Integer maxFailed = activationProvider.getMaxFailedAttemptCount(identity, customAttributes, userId, ActivationType.CUSTOM, context); + final Long maxFailedCount = maxFailed == null ? null : maxFailed.longValue(); + final Long activationValidityPeriod = activationProvider.getValidityPeriodDuringActivation(identity, customAttributes, userId, ActivationType.CUSTOM, context); + Date activationExpire = null; + if (activationValidityPeriod != null) { + final Instant expiration = Instant.now().plusMillis(activationValidityPeriod); + activationExpire = Date.from(expiration); + } + + final EciesEncryptedRequest activationData = request.getActivationData(); + + // Create activation for a looked up user and application related to the given application key + final CreateActivationRequest createRequest = new CreateActivationRequest(); + createRequest.setUserId(userId); + createRequest.setTimestampActivationExpire(activationExpire); + createRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes); + createRequest.setMaxFailureCount(maxFailedCount); + createRequest.setApplicationKey(eciesContext.getApplicationKey()); + createRequest.setTemporaryKeyId(activationData.getTemporaryKeyId()); + createRequest.setEphemeralPublicKey(activationData.getEphemeralPublicKey()); + createRequest.setEncryptedData(activationData.getEncryptedData()); + createRequest.setMac(activationData.getMac()); + createRequest.setNonce(activationData.getNonce()); + createRequest.setProtocolVersion(eciesContext.getVersion()); + createRequest.setTimestamp(activationData.getTimestamp()); + final CreateActivationResponse response = powerAuthClient.createActivation( + createRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + + final String activationId = response.getActivationId(); + final String applicationId = response.getApplicationId(); + + final UserInfoContext userInfoContext = UserInfoContext.builder() + .stage(UserInfoStage.ACTIVATION_PROCESS_CUSTOM) + .userId(userId) + .activationId(activationId) + .applicationId(applicationId) + .build(); + final Map userInfo = processUserInfo(userInfoContext); + + // Process custom attributes using a custom logic + final Map processedCustomAttributes = activationProvider.processCustomActivationAttributes(customAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context); + + // Save activation flags in case the provider specified any flags + final List activationFlags = activationProvider.getActivationFlags(identity, processedCustomAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context); + if (activationFlags != null && !activationFlags.isEmpty()) { + final AddActivationFlagsRequest flagsRequest = new AddActivationFlagsRequest(); + flagsRequest.setActivationId(activationId); + flagsRequest.getActivationFlags().addAll(activationFlags); + powerAuthClient.addActivationFlags( + flagsRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + } + + // Check if activation should be committed instantly and if yes, perform commit + if (activationProvider.shouldAutoCommitActivation(identity, customAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context)) { + final CommitActivationResponse commitResponse = commitActivation(activationId); + if (commitResponse.isActivated()) { + activationProvider.activationWasCommitted(identity, customAttributes, activationId, userId, applicationId, ActivationType.CUSTOM, context); + } + } + + // Prepare encrypted activation data + return prepareEncryptedResponse(response.getEncryptedData(), response.getMac(), + response.getNonce(), response.getTimestamp(), processedCustomAttributes, userInfo); + } + private static void checkIdentityAttributesPresent(final Map identity) throws PowerAuthActivationException { if (CollectionUtils.isEmpty(identity)) { throw new PowerAuthActivationException("Identity attributes are missing for activation."); @@ -606,13 +611,13 @@ public ActivationRemoveResponse removeActivation(PowerAuthApiAuthentication apiA * @param processedCustomAttributes Custom attributes to be returned. * @return Encrypted response object. */ - private ActivationLayer1Response prepareEncryptedResponse(String encryptedData, String mac, String nonce, Long timestmap, Map processedCustomAttributes, Map userInfo) { + private ActivationLayer1Response prepareEncryptedResponse(String encryptedData, String mac, String nonce, Long timestamp, Map processedCustomAttributes, Map userInfo) { // Prepare encrypted response object for layer 2 final EciesEncryptedResponse encryptedResponseL2 = new EciesEncryptedResponse(); encryptedResponseL2.setEncryptedData(encryptedData); encryptedResponseL2.setMac(mac); encryptedResponseL2.setNonce(nonce); - encryptedResponseL2.setTimestamp(timestmap); + encryptedResponseL2.setTimestamp(timestamp); // The response is encrypted once more before sent to client using ResponseBodyAdvice final ActivationLayer1Response responseL1 = new ActivationLayer1Response(); From 088eda1d1079fe21ac2404bc095988050fe873a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Mon, 9 Sep 2024 15:00:32 +0200 Subject: [PATCH 10/19] Fix #532: OIDC: Implement activation using OAuth 2.0, openid scope (#547) * Fix #532: OIDC: Implement activation using OAuth 2.0, openid scope --- pom.xml | 8 + .../rest/api/model/entity/ActivationType.java | 8 + powerauth-restful-security-spring/pom.xml | 15 +- ...AuthApplicationConfigurationException.java | 65 +++++++ .../api/spring/service/ActivationService.java | 83 ++++++++- .../oidc/ClientAuthenticationMethod.java | 37 ++++ .../spring/service/oidc/IdTokenValidator.java | 82 +++++++++ ...OidcActivationConfigurationProperties.java | 44 +++++ .../service/oidc/OidcActivationContext.java | 39 ++++ .../oidc/OidcApplicationConfiguration.java | 62 +++++++ .../OidcApplicationConfigurationService.java | 108 +++++++++++ .../service/oidc/OidcConfigurationQuery.java | 33 ++++ .../api/spring/service/oidc/OidcHandler.java | 173 ++++++++++++++++++ .../spring/service/oidc/OidcTokenClient.java | 98 ++++++++++ .../api/spring/service/oidc/TokenRequest.java | 38 ++++ .../spring/service/oidc/TokenResponse.java | 43 +++++ .../service/oidc/IdTokenValidatorTest.java | 107 +++++++++++ ...dcApplicationConfigurationServiceTest.java | 154 ++++++++++++++++ 18 files changed, 1193 insertions(+), 4 deletions(-) create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthApplicationConfigurationException.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/ClientAuthenticationMethod.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/IdTokenValidator.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationConfigurationProperties.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationContext.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationService.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcConfigurationQuery.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcHandler.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcTokenClient.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenRequest.java create mode 100644 powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenResponse.java create mode 100644 powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/IdTokenValidatorTest.java create mode 100644 powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationServiceTest.java diff --git a/pom.xml b/pom.xml index e6a92cc0..747e481d 100644 --- a/pom.xml +++ b/pom.xml @@ -131,6 +131,14 @@ lombok provided + + + + io.netty + netty-resolver-dns-native-macos + runtime + osx-aarch_64 +
    diff --git a/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/entity/ActivationType.java b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/entity/ActivationType.java index 395b16de..2f401d69 100644 --- a/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/entity/ActivationType.java +++ b/powerauth-restful-model/src/main/java/io/getlime/security/powerauth/rest/api/model/entity/ActivationType.java @@ -33,9 +33,17 @@ public enum ActivationType { /** * Activation via custom credentials. + * @deprecated Use {@link #DIRECT} instead. */ + @Deprecated CUSTOM, + /** + * Direct activation, alias for {@link #CUSTOM}. + * The method could be specified, for example {@code OIDC}. + */ + DIRECT, + /** * Activation via recovery code. */ diff --git a/powerauth-restful-security-spring/pom.xml b/powerauth-restful-security-spring/pom.xml index a97691aa..016b4a74 100644 --- a/powerauth-restful-security-spring/pom.xml +++ b/powerauth-restful-security-spring/pom.xml @@ -54,10 +54,21 @@ + + + org.springframework.security + spring-security-oauth2-client + + + + org.springframework.security + spring-security-oauth2-jose + + - org.junit.jupiter - junit-jupiter-engine + org.springframework.boot + spring-boot-starter-test test diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthApplicationConfigurationException.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthApplicationConfigurationException.java new file mode 100644 index 00000000..031286d1 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/exception/PowerAuthApplicationConfigurationException.java @@ -0,0 +1,65 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.exception; + +import java.io.Serial; + +/** + * Exception related to application configuration. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +public class PowerAuthApplicationConfigurationException extends Exception { + + @Serial + private static final long serialVersionUID = 8677977961740746599L; + + /** + * No-arg constructor. + */ + public PowerAuthApplicationConfigurationException() { + super(); + } + + /** + * Constructor with a custom error message. + * @param message Error message. + */ + public PowerAuthApplicationConfigurationException(String message) { + super(message); + } + + /** + * Constructor with a cause. + * @param cause Error cause. + */ + public PowerAuthApplicationConfigurationException(Throwable cause) { + super(cause); + } + + /** + * Constructor with a message and cause. + * @param message Error message. + * @param cause Error cause. + */ + public PowerAuthApplicationConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java index e20748ba..0d7ca20a 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java @@ -42,6 +42,8 @@ import io.getlime.security.powerauth.rest.api.spring.model.UserInfoContext; import io.getlime.security.powerauth.rest.api.spring.provider.CustomActivationProvider; import io.getlime.security.powerauth.rest.api.spring.provider.UserInfoProvider; +import io.getlime.security.powerauth.rest.api.spring.service.oidc.OidcActivationContext; +import io.getlime.security.powerauth.rest.api.spring.service.oidc.OidcHandler; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -65,9 +67,12 @@ @Slf4j public class ActivationService { + private static final String METHOD_OIDC = "oidc"; + private final PowerAuthClient powerAuthClient; private final HttpCustomizationService httpCustomizationService; private final ActivationContextConverter activationContextConverter; + private final OidcHandler oidcHandler; private PowerAuthApplicationConfiguration applicationConfiguration; private CustomActivationProvider activationProvider; @@ -81,10 +86,16 @@ public class ActivationService { * @param activationContextConverter Activation context converter. */ @Autowired - public ActivationService(PowerAuthClient powerAuthClient, HttpCustomizationService httpCustomizationService, ActivationContextConverter activationContextConverter) { + public ActivationService( + PowerAuthClient powerAuthClient, + HttpCustomizationService httpCustomizationService, + ActivationContextConverter activationContextConverter, + OidcHandler oidcHandler) { + this.powerAuthClient = powerAuthClient; this.httpCustomizationService = httpCustomizationService; this.activationContextConverter = activationContextConverter; + this.oidcHandler = oidcHandler; } /** @@ -134,7 +145,8 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request return switch (type) { // Regular activation which uses "code" identity attribute case CODE -> processCodeActivation(eciesContext, request); - case CUSTOM -> processCustomActivation(eciesContext, request); + // Direct activation for known specific methods, otherwise fallback to custom activation + case CUSTOM, DIRECT -> processDirectOrCustomActivation(eciesContext, request, type, identity); case RECOVERY -> processRecoveryCodeActivation(eciesContext, request); }; } catch (PowerAuthClientException ex) { @@ -430,6 +442,73 @@ private ActivationLayer1Response processCustomActivation(final EncryptionContext response.getNonce(), response.getTimestamp(), processedCustomAttributes, userInfo); } + private ActivationLayer1Response processDirectOrCustomActivation(final EncryptionContext eciesContext, final ActivationLayer1Request request, final ActivationType type, final Map identity) throws PowerAuthActivationException, PowerAuthClientException { + if (type == ActivationType.DIRECT) { + final String method = identity.get("method"); + if (METHOD_OIDC.equals(method)) { + return processOidcActivation(eciesContext, request); + } else { + logger.info("Unknown method: {} of direct activation, fallback to custom activation", method); + } + } + + return processCustomActivation(eciesContext, request); + } + + private ActivationLayer1Response processOidcActivation(final EncryptionContext eciesContext, final ActivationLayer1Request request) throws PowerAuthClientException, PowerAuthActivationException { + logger.debug("Processing direct OIDC activation."); + + final Map identity = request.getIdentityAttributes(); + final OidcActivationContext oAuthActivationContext = OidcActivationContext.builder() + .providerId(identity.get("providerId")) + .code(identity.get("code")) + .nonce(identity.get("nonce")) + .applicationKey(eciesContext.getApplicationKey()) + .build(); + + final String userId = oidcHandler.retrieveUserId(oAuthActivationContext); + + // Create context for passing parameters between activation provider calls + final Map context = new LinkedHashMap<>(); + + final EciesEncryptedRequest activationData = request.getActivationData(); + final Map customAttributes = Objects.requireNonNullElse(request.getCustomAttributes(), new HashMap<>()); + + final CreateActivationRequest createRequest = new CreateActivationRequest(); + createRequest.setUserId(userId); + createRequest.setGenerateRecoveryCodes(shouldGenerateRecoveryCodes(identity, customAttributes, context)); + createRequest.setApplicationKey(eciesContext.getApplicationKey()); + createRequest.setTemporaryKeyId(activationData.getTemporaryKeyId()); + createRequest.setEphemeralPublicKey(activationData.getEphemeralPublicKey()); + createRequest.setEncryptedData(activationData.getEncryptedData()); + createRequest.setMac(activationData.getMac()); + createRequest.setNonce(activationData.getNonce()); + createRequest.setProtocolVersion(eciesContext.getVersion()); + createRequest.setTimestamp(activationData.getTimestamp()); + + final CreateActivationResponse response = powerAuthClient.createActivation( + createRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + + final String activationId = response.getActivationId(); + final String applicationId = response.getApplicationId(); + + commitActivation(activationId); + + final UserInfoContext userInfoContext = UserInfoContext.builder() + .stage(UserInfoStage.ACTIVATION_PROCESS_CUSTOM) + .userId(userId) + .activationId(activationId) + .applicationId(applicationId) + .build(); + final Map userInfo = processUserInfo(userInfoContext); + + return prepareEncryptedResponse(response.getEncryptedData(), response.getMac(), + response.getNonce(), response.getTimestamp(), customAttributes, userInfo); + } + private static void checkIdentityAttributesPresent(final Map identity) throws PowerAuthActivationException { if (CollectionUtils.isEmpty(identity)) { throw new PowerAuthActivationException("Identity attributes are missing for activation."); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/ClientAuthenticationMethod.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/ClientAuthenticationMethod.java new file mode 100644 index 00000000..cd03a641 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/ClientAuthenticationMethod.java @@ -0,0 +1,37 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * OIDC client authentication methods. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +enum ClientAuthenticationMethod { + + @JsonProperty("client_secret_basic") + CLIENT_SECRET_BASIC, + + @JsonProperty("client_secret_post") + CLIENT_SECRET_POST + +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/IdTokenValidator.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/IdTokenValidator.java new file mode 100644 index 00000000..90c72fa6 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/IdTokenValidator.java @@ -0,0 +1,82 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +/** + * Additional ID token validations. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Slf4j +final class IdTokenValidator { + + private IdTokenValidator() { + throw new IllegalStateException("Should not be instantiated"); + } + + static boolean isAtHashValid(final Jwt idToken, final String accessToken) { + final String atHash = idToken.getClaimAsString("at_hash"); + return atHash == null || isAtHashValid(accessToken, atHash, idToken.getHeaders().get("alg").toString()); + } + + static boolean isNonceValid(final Jwt idToken, final String nonce) { + return nonce.equals(idToken.getClaimAsString("nonce")); + } + + /** + *
      + *
    1. Hash the octets of the ASCII representation of the access_token with the hash algorithm for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256.
    2. + *
    3. Take the left-most half of the hash and base64url-encode it.
    4. + *
    5. The value of at_hash in the ID Token MUST match the value produced in the previous step.
    6. + *
    + * + * @see 3.2.2.9. Access Token Validation + */ + private static boolean isAtHashValid(final String accessToken, final String atHash, final String signatureAlgorithm) { + try { + final MessageDigest digest = MessageDigest.getInstance(mapHashAlgorithm(signatureAlgorithm)); + final byte[] hash = digest.digest(accessToken.getBytes()); + final byte[] leftHalf = new byte[hash.length / 2]; + System.arraycopy(hash, 0, leftHalf, 0, leftHalf.length); + final String computedAtHash = Base64.getUrlEncoder().withoutPadding().encodeToString(leftHalf); + return atHash.equals(computedAtHash); + } catch (NoSuchAlgorithmException e) { + logger.error("Unable to validate at_hash", e); + return false; + } + } + + private static String mapHashAlgorithm(final String signatureAlgorithm) throws NoSuchAlgorithmException { + return switch (signatureAlgorithm) { + case JwsAlgorithms.RS256, JwsAlgorithms.ES256 -> "SHA-256"; + case JwsAlgorithms.RS384, JwsAlgorithms.ES384 -> "SHA-384"; + case JwsAlgorithms.RS512, JwsAlgorithms.ES512 -> "SHA-512"; + default -> throw new NoSuchAlgorithmException("Unsupported signature algorithm: " + signatureAlgorithm); + }; + } +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationConfigurationProperties.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationConfigurationProperties.java new file mode 100644 index 00000000..ea1c4e79 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationConfigurationProperties.java @@ -0,0 +1,44 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import com.wultra.core.rest.client.base.RestClientConfiguration; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * OIDC (OpenID Connect) token endpoint request. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Component +@ConfigurationProperties(prefix = "activation.oidc") +@Getter +@Setter +class OidcActivationConfigurationProperties { + + /** + * REST client configuration + */ + private RestClientConfiguration restClientConfig = new RestClientConfiguration(); + +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationContext.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationContext.java new file mode 100644 index 00000000..c4e10a64 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationContext.java @@ -0,0 +1,39 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import lombok.Builder; +import lombok.Getter; + +/** + * OIDC (OpenID Connect) activation context. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Builder +@Getter +public class OidcActivationContext { + + private String code; + private String nonce; + private String applicationKey; + private String providerId; + +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java new file mode 100644 index 00000000..c78b20c1 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java @@ -0,0 +1,62 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import lombok.Getter; +import lombok.Setter; + +/** + * OIDC activation configuration. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Getter +@Setter +public class OidcApplicationConfiguration { + + private String providerId; + + private String clientId; + + private String clientSecret; + + /** + * Optional. If emtpy, {@code client_secret_basic} is used. + */ + private ClientAuthenticationMethod clientAuthenticationMethod; + + private String issuerUri; + + private String tokenUri; + + private String jwkSetUri; + + private String redirectUri; + + private String scopes; + + private String authorizeUri; + + /** + * Optional. If empty, {code RS256} is used. + */ + private String signatureAlgorithm; + +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationService.java new file mode 100644 index 00000000..1c79cfe1 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationService.java @@ -0,0 +1,108 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wultra.security.powerauth.client.PowerAuthClient; +import com.wultra.security.powerauth.client.model.entity.ApplicationConfigurationItem; +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.client.model.request.GetApplicationConfigRequest; +import com.wultra.security.powerauth.client.model.request.LookupApplicationByAppKeyRequest; +import com.wultra.security.powerauth.client.model.response.GetApplicationConfigResponse; +import com.wultra.security.powerauth.client.model.response.LookupApplicationByAppKeyResponse; +import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthApplicationConfigurationException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; + +/** + * Application configuration service for OIDC. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Component +@AllArgsConstructor +@Slf4j +public class OidcApplicationConfigurationService { + + private static final String OAUTH2_PROVIDERS = "oauth2_providers"; + + private final PowerAuthClient powerAuthClient; + + private final ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + /** + * Provide OIDC application configuration. + * + * @param request Query object. + * @return OIDC application configuration + * @throws PowerAuthApplicationConfigurationException in case of error. + */ + public OidcApplicationConfiguration fetchOidcApplicationConfiguration(final OidcConfigurationQuery request) throws PowerAuthApplicationConfigurationException { + try { + final String applicationId = fetchApplicationIdByApplicationKey(request.applicationKey()); + + final GetApplicationConfigRequest configRequest = new GetApplicationConfigRequest(); + configRequest.setApplicationId(applicationId); + + final GetApplicationConfigResponse applicationConfig = powerAuthClient.getApplicationConfig(configRequest); + return applicationConfig.getApplicationConfigs().stream() + .filter(it -> it.getKey().equals(OAUTH2_PROVIDERS)) + .findFirst() + .map(ApplicationConfigurationItem::getValues) + .map(it -> convert(it, request.providerId())) + .orElseThrow(() -> + new PowerAuthApplicationConfigurationException("Fetching application configuration failed, application ID: %s, provider ID: %s".formatted(applicationId, request.providerId()))); + } catch (PowerAuthClientException e) { + throw new PowerAuthApplicationConfigurationException("Fetching application configuration failed.", e); + } + } + + private OidcApplicationConfiguration convert(List values, String providerId) { + return values.stream() + .map(this::convert) + .filter(Objects::nonNull) + .filter(it -> it.getProviderId().equals(providerId)) + .findFirst() + .orElse(null); + } + + private OidcApplicationConfiguration convert(Object value) { + try { + return objectMapper.convertValue(value, OidcApplicationConfiguration.class); + } catch (IllegalArgumentException e) { + logger.warn("Unable to convert {}", value, e); + return null; + } + } + + private String fetchApplicationIdByApplicationKey(final String applicationKey) throws PowerAuthClientException { + final LookupApplicationByAppKeyRequest request = new LookupApplicationByAppKeyRequest(); + request.setApplicationKey(applicationKey); + + final LookupApplicationByAppKeyResponse applicationResponse = powerAuthClient.lookupApplicationByAppKey(request); + return applicationResponse.getApplicationId(); + } + +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcConfigurationQuery.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcConfigurationQuery.java new file mode 100644 index 00000000..4017b843 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcConfigurationQuery.java @@ -0,0 +1,33 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import lombok.Builder; + +/** + * Query for {@link OidcApplicationConfigurationService}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + * @param providerId + * @param applicationKey + */ +@Builder +public record OidcConfigurationQuery(String providerId, String applicationKey) { +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcHandler.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcHandler.java new file mode 100644 index 00000000..399590d7 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcHandler.java @@ -0,0 +1,173 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import com.wultra.core.rest.client.base.RestClientException; +import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthActivationException; +import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthApplicationConfigurationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrations; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Wrap OIDC (OpenID Connect) client calls, add other logic such as validation. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Component +@Slf4j +public class OidcHandler { + + private final Map signatureAlgorithms = new ConcurrentHashMap<>(); + + private final OidcIdTokenDecoderFactory oidcIdTokenDecoderFactory; + + private final OidcTokenClient tokenClient; + + private final OidcApplicationConfigurationService applicationConfigurationService; + + @Autowired + OidcHandler(final OidcTokenClient tokenClient, final OidcApplicationConfigurationService applicationConfigurationService) { + this.tokenClient = tokenClient; + this.applicationConfigurationService = applicationConfigurationService; + oidcIdTokenDecoderFactory = new OidcIdTokenDecoderFactory(); + oidcIdTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> signatureAlgorithms.get(clientRegistration.getRegistrationId())); + } + + /** + * Retrieve user ID from a token, using {@code authorization_code} flow. The token is verified first. + * + * @param request Parameter object. + * @return User ID. + * @throws PowerAuthActivationException in case of error. + */ + public String retrieveUserId(final OidcActivationContext request) throws PowerAuthActivationException { + final OidcApplicationConfiguration oidcApplicationConfiguration = fetchOidcApplicationConfiguration(request); + + final ClientRegistration clientRegistration = createClientRegistration(request.getProviderId(), oidcApplicationConfiguration); + + signatureAlgorithms.putIfAbsent(clientRegistration.getRegistrationId(), mapSignatureAlgorithmFromConfiguration(oidcApplicationConfiguration)); + + final TokenRequest tokenRequest = TokenRequest.builder() + .code(request.getCode()) + .clientRegistration(clientRegistration) + .build(); + + final TokenResponse tokenResponse = fetchToken(tokenRequest); + final Jwt idToken = verifyAndDecode(tokenResponse, clientRegistration, request.getNonce()); + + return idToken.getSubject(); + } + + private static ClientRegistration createClientRegistration(final String providerId, final OidcApplicationConfiguration oidcApplicationConfiguration) { + logger.debug("Trying to configure via {}/.well-known/openid-configuration", oidcApplicationConfiguration.getIssuerUri()); + try { + return ClientRegistrations.fromOidcIssuerLocation(oidcApplicationConfiguration.getIssuerUri()) + .clientId(oidcApplicationConfiguration.getClientId()) + .clientSecret(oidcApplicationConfiguration.getClientSecret()) + .redirectUri(oidcApplicationConfiguration.getRedirectUri()) + .build(); + } catch (Exception e) { + logger.info("Unable to reach {}/.well-known/openid-configuration, fallback to manual config; {}", oidcApplicationConfiguration.getIssuerUri(), e.getMessage()); + logger.debug("Unable to reach {}/.well-known/openid-configuration", oidcApplicationConfiguration.getIssuerUri(), e); + return ClientRegistration.withRegistrationId(providerId) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .clientId(oidcApplicationConfiguration.getClientId()) + .clientSecret(oidcApplicationConfiguration.getClientSecret()) + .clientAuthenticationMethod(convert(oidcApplicationConfiguration.getClientAuthenticationMethod())) + .tokenUri(oidcApplicationConfiguration.getTokenUri()) + .jwkSetUri(oidcApplicationConfiguration.getJwkSetUri()) + .authorizationUri(oidcApplicationConfiguration.getAuthorizeUri()) + .redirectUri(oidcApplicationConfiguration.getRedirectUri()) + .build(); + } + } + + private static ClientAuthenticationMethod convert(final io.getlime.security.powerauth.rest.api.spring.service.oidc.ClientAuthenticationMethod source) { + return switch(source) { + case CLIENT_SECRET_POST -> ClientAuthenticationMethod.CLIENT_SECRET_POST; + case CLIENT_SECRET_BASIC -> ClientAuthenticationMethod.CLIENT_SECRET_BASIC; + }; + } + + private OidcApplicationConfiguration fetchOidcApplicationConfiguration(final OidcActivationContext request) throws PowerAuthActivationException { + try { + return applicationConfigurationService.fetchOidcApplicationConfiguration(OidcConfigurationQuery.builder() + .applicationKey(request.getApplicationKey()) + .providerId(request.getProviderId()) + .build()); + } catch (PowerAuthApplicationConfigurationException e) { + throw new PowerAuthActivationException(e); + } + } + + private TokenResponse fetchToken(final TokenRequest tokenRequest) throws PowerAuthActivationException { + final String clientId = tokenRequest.getClientRegistration().getClientId(); + logger.debug("Fetching token, clientId: {}", clientId); + try { + final TokenResponse response = tokenClient.fetchTokenResponse(tokenRequest); + logger.debug("Token fetched, verifying, clientId: {}", clientId); + return response; + } catch (RestClientException e) { + throw new PowerAuthActivationException("Unable to get token response", e); + } + } + + private Jwt verifyAndDecode(final TokenResponse tokenResponse, final ClientRegistration clientRegistration, final String nonce) throws PowerAuthActivationException { + final JwtDecoder jwtDecoder = oidcIdTokenDecoderFactory.createDecoder(clientRegistration); + + try { + final Jwt idTokenJwt = jwtDecoder.decode(tokenResponse.getIdToken()); + validate(idTokenJwt, nonce, tokenResponse); + return idTokenJwt; + } catch (JwtException e) { + throw new PowerAuthActivationException("Decoding JWT failed", e); + } + } + + private static void validate(final Jwt idTokenJwt, final String nonce, final TokenResponse tokenResponse) throws PowerAuthActivationException { + if (!IdTokenValidator.isNonceValid(idTokenJwt, nonce)) { + throw new PowerAuthActivationException("The nonce does not match"); + } + if (!IdTokenValidator.isAtHashValid(idTokenJwt, tokenResponse.getAccessToken())) { + throw new PowerAuthActivationException("The at_hash does not match"); + } + } + + private static SignatureAlgorithm mapSignatureAlgorithmFromConfiguration(final OidcApplicationConfiguration oidcApplicationConfiguration) { + final String signatureAlgorithmString = oidcApplicationConfiguration.getSignatureAlgorithm(); + final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(signatureAlgorithmString); + return Objects.requireNonNullElse(signatureAlgorithm, SignatureAlgorithm.RS256); + } + +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcTokenClient.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcTokenClient.java new file mode 100644 index 00000000..9a7fdb61 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcTokenClient.java @@ -0,0 +1,98 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import com.wultra.core.rest.client.base.DefaultRestClient; +import com.wultra.core.rest.client.base.RestClient; +import com.wultra.core.rest.client.base.RestClientConfiguration; +import com.wultra.core.rest.client.base.RestClientException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * OIDC (OpenID Connect) client. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Component +@AllArgsConstructor +@Slf4j +class OidcTokenClient { + + private OidcActivationConfigurationProperties configurationProperties; + + /** + * Call token endpoint using {@code authorization_code} flow. Mind that the token is not verified yet. + * + * @param tokenRequest Token request. + * @return Token response. + * @throws RestClientException in case of error. + */ + TokenResponse fetchTokenResponse(final TokenRequest tokenRequest) throws RestClientException { + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + final org.springframework.security.oauth2.core.ClientAuthenticationMethod clientAuthenticationMethod = tokenRequest.getClientRegistration().getClientAuthenticationMethod(); + logger.debug("Using ClientAuthenticationMethod: {}", clientAuthenticationMethod); + + final ClientRegistration clientRegistration = tokenRequest.getClientRegistration(); + + final MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("grant_type", "authorization_code"); + map.add("client_id", clientRegistration.getClientId()); + map.add("code", tokenRequest.getCode()); + map.add("redirect_uri", clientRegistration.getRedirectUri()); + + if (clientAuthenticationMethod == org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_POST) { + map.add("client_secret", clientRegistration.getClientSecret()); + } + + final RestClient restClient = createRestClient(tokenRequest); + + final String tokenUrl = clientRegistration.getProviderDetails().getTokenUri(); + logger.debug("Calling token endpoint: {}", tokenUrl); + final ResponseEntity response = restClient.post(tokenUrl, map, null, headers, new ParameterizedTypeReference<>(){}); + logger.debug("Token endpoint call finished: {}", tokenUrl); + + if (response == null) { + throw new RestClientException("Response is null"); + } + + return response.getBody(); + } + + private RestClient createRestClient(final TokenRequest tokenRequest) throws RestClientException { + final RestClientConfiguration restClientConfiguration = configurationProperties.getRestClientConfig(); + final org.springframework.security.oauth2.core.ClientAuthenticationMethod clientAuthenticationMethod = tokenRequest.getClientRegistration().getClientAuthenticationMethod(); + restClientConfiguration.setHttpBasicAuthEnabled(clientAuthenticationMethod == null || clientAuthenticationMethod == org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + restClientConfiguration.setHttpBasicAuthUsername(tokenRequest.getClientRegistration().getClientId()); + restClientConfiguration.setHttpBasicAuthPassword(tokenRequest.getClientRegistration().getClientSecret()); + + return new DefaultRestClient(restClientConfiguration); + } +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenRequest.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenRequest.java new file mode 100644 index 00000000..b5f6372c --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenRequest.java @@ -0,0 +1,38 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.security.oauth2.client.registration.ClientRegistration; + +/** + * OIDC token endpoint request. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Builder +@Getter +class TokenRequest { + + private String code; + private ClientRegistration clientRegistration; + +} diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenResponse.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenResponse.java new file mode 100644 index 00000000..b21774ee --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenResponse.java @@ -0,0 +1,43 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +/** + * OIDC token endpoint response. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@Getter +@Setter +public class TokenResponse { + + @JsonProperty("id_token") + private String idToken; + + @JsonProperty("access_token") + private String accessToken; + +} diff --git a/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/IdTokenValidatorTest.java b/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/IdTokenValidatorTest.java new file mode 100644 index 00000000..3aa8b5d7 --- /dev/null +++ b/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/IdTokenValidatorTest.java @@ -0,0 +1,107 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; + +import java.text.ParseException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test for {@link IdTokenValidator}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +class IdTokenValidatorTest { + + // test vector from https://stackoverflow.com/a/36708354/204950 + private static final String ACCESS_TOKEN = "ya29.eQGmYe6H3fP_d65AY0pOMCFikA0f4hzVZGmTPPyv7k_l6HzlEIpFXnXGZjcMhkyyuqSMtN_RTGJ-xg"; + + /* + jwt.io + { + "sub": "1234567890", + "name": "John Doe", + "aud": "pas", + "nonce": "a184d4a4sd7asd74a8sda", + "at_hash": "lOtI0BRou0Z4LPtQuE8cCw" + } + */ + private static final String ID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYXVkIjoicGFzIiwibm9uY2UiOiJhMTg0ZDRhNHNkN2FzZDc0YThzZGEiLCJhdF9oYXNoIjoibE90STBCUm91MFo0TFB0UXVFOGNDdyJ9.KZqKWSu4fD8s95E5l4Z8qqHLo5iOeu4Ks4NPMRHhhdDqszXREDrRF9nTVOiJMrYVeYnI7dPtixtL9JPyODyYAQ070Qa0bkvJ2-OTSlESgVuO62QgRXP8Ba_uN_UT_xLRKoSbgPstuv5tjHT34iugYy48Meheraoj5v-QDo8glltiWR8Bo_WOz4SrtHezD4DqKRsnE2DlTYkVqmqK8s-wgik67JhFygupSBLsmMi1zRWjThjFibWRR31kFDc1jRuUWl1RidYPHMIZkUvMT3GQWL0B45ET1-fhrpg_GQZtlLadADb24QtY06X2peyFZ3JrvYIsxf4F2R1F6UUDzDcN0g"; + + private final JwtDecoder jwtDecoder = token -> { + try { + final SignedJWT signedJWT = SignedJWT.parse(token); + final JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet(); + final JWSHeader header = signedJWT.getHeader(); + + return Jwt.withTokenValue(token) + .headers(headers -> headers.putAll(header.toJSONObject())) + .claims(claims -> claims.putAll(claimsSet.getClaims())) + .build(); + } catch (ParseException e) { + throw new RuntimeException("Invalid token", e); + } + }; + + @Test + void testNonceValidator_success() { + final Jwt jwt = jwtDecoder.decode(ID_TOKEN); + + final boolean result = IdTokenValidator.isNonceValid(jwt, "a184d4a4sd7asd74a8sda"); + + assertTrue(result); + } + + @Test + void testNonceValidator_invalid() { + final Jwt jwt = jwtDecoder.decode(ID_TOKEN); + + final boolean result = IdTokenValidator.isNonceValid(jwt, "invalid"); + + assertFalse(result); + } + + @Test + void testAtHash_success() { + final Jwt jwt = jwtDecoder.decode(ID_TOKEN); + + final boolean result = IdTokenValidator.isAtHashValid(jwt, ACCESS_TOKEN); + + assertTrue(result); + } + + @Test + void testAtHashValidator_invalid() { + final Jwt jwt = jwtDecoder.decode(ID_TOKEN); + + final boolean result = IdTokenValidator.isAtHashValid(jwt, "invalid access token"); + + assertFalse(result); + } + +} \ No newline at end of file diff --git a/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationServiceTest.java b/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationServiceTest.java new file mode 100644 index 00000000..9b36cd50 --- /dev/null +++ b/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationServiceTest.java @@ -0,0 +1,154 @@ +/* + * PowerAuth integration libraries for RESTful API applications, examples and + * related software components + * + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package io.getlime.security.powerauth.rest.api.spring.service.oidc; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wultra.security.powerauth.client.PowerAuthClient; +import com.wultra.security.powerauth.client.model.request.GetApplicationConfigRequest; +import com.wultra.security.powerauth.client.model.request.LookupApplicationByAppKeyRequest; +import com.wultra.security.powerauth.client.model.response.GetApplicationConfigResponse; +import com.wultra.security.powerauth.client.model.response.LookupApplicationByAppKeyResponse; +import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthApplicationConfigurationException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +/** + * Test for {@link OidcApplicationConfigurationService}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@ExtendWith(MockitoExtension.class) +class OidcApplicationConfigurationServiceTest { + + @Mock + private PowerAuthClient powerAuthClient; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @InjectMocks + private OidcApplicationConfigurationService tested; + + @Test + void testFetchOidcApplicationConfiguration() throws Exception { + final LookupApplicationByAppKeyRequest lookupRequest = new LookupApplicationByAppKeyRequest(); + lookupRequest.setApplicationKey("AIsOlIghnLztV2np3SANnQ=="); + + final LookupApplicationByAppKeyResponse lookupResponse = new LookupApplicationByAppKeyResponse(); + lookupResponse.setApplicationId("application-1"); + + when(powerAuthClient.lookupApplicationByAppKey(lookupRequest)) + .thenReturn(lookupResponse); + + final GetApplicationConfigRequest configRequest = new GetApplicationConfigRequest(); + configRequest.setApplicationId("application-1"); + + final GetApplicationConfigResponse configResponse = createResponse(); + when(powerAuthClient.getApplicationConfig(configRequest)) + .thenReturn(configResponse); + + final OidcApplicationConfiguration result = tested.fetchOidcApplicationConfiguration(OidcConfigurationQuery.builder() + .applicationKey("AIsOlIghnLztV2np3SANnQ==") + .providerId("xyz999") + .build()); + + assertEquals("xyz999", result.getProviderId()); + assertEquals("jabberwocky", result.getClientId()); + assertEquals("https://redirect.example.com", result.getRedirectUri()); + assertEquals("https://issuer.example.com", result.getIssuerUri()); + assertEquals("openid", result.getScopes()); + assertEquals("https://token.example.com", result.getTokenUri()); + assertEquals("https://authorize.example.com", result.getAuthorizeUri()); + assertEquals("ES256", result.getSignatureAlgorithm()); + } + + @Test + void testFetchOidcApplicationConfiguration_invalidProviderId() throws Exception { + final LookupApplicationByAppKeyRequest lookupRequest = new LookupApplicationByAppKeyRequest(); + lookupRequest.setApplicationKey("AIsOlIghnLztV2np3SANnQ=="); + + final LookupApplicationByAppKeyResponse lookupResponse = new LookupApplicationByAppKeyResponse(); + lookupResponse.setApplicationId("application-1"); + + when(powerAuthClient.lookupApplicationByAppKey(lookupRequest)) + .thenReturn(lookupResponse); + + final GetApplicationConfigRequest configRequest = new GetApplicationConfigRequest(); + configRequest.setApplicationId("application-1"); + + final GetApplicationConfigResponse configResponse = createResponse(); + when(powerAuthClient.getApplicationConfig(configRequest)) + .thenReturn(configResponse); + + final Exception e = assertThrows(PowerAuthApplicationConfigurationException.class, () -> tested.fetchOidcApplicationConfiguration(OidcConfigurationQuery.builder() + .applicationKey("AIsOlIghnLztV2np3SANnQ==") + .providerId("non-existing") + .build())); + + assertEquals("Fetching application configuration failed, application ID: application-1, provider ID: non-existing", e.getMessage()); + } + + private GetApplicationConfigResponse createResponse() throws JsonProcessingException { + final String json = """ + { + "applicationId": "application-1", + "applicationConfigs": [ + { + "key": "oauth2_providers", + "values": [ + { + "providerId": "abc123", + "clientId": "1234567890abcdef", + "clientSecret": "top secret", + "scopes": "openid", + "authorizeUri": "https://...", + "redirectUri": "https://...", + "tokenUri": "https://...", + "issuerUri": "https://...", + "userInfoUri": "https://..." + }, + { + "providerId": "xyz999", + "clientId": "jabberwocky", + "clientSecret": "top secret", + "scopes": "openid", + "authorizeUri": "https://authorize.example.com", + "redirectUri": "https://redirect.example.com", + "issuerUri": "https://issuer.example.com", + "tokenUri": "https://token.example.com", + "userInfoUri": "https://...", + "signatureAlgorithm": "ES256" + } + ] + } + ] + } + """; + + return objectMapper.readValue(json, GetApplicationConfigResponse.class); + } +} \ No newline at end of file From fb4b7660f570807b213f3424c2e15bfb6aa6ef4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Tue, 10 Sep 2024 10:47:32 +0200 Subject: [PATCH 11/19] Fix #534: OIDC: Implement PKCE extension (#554) * Fix #534: OIDC: Implement PKCE extension --- .../rest/api/spring/service/ActivationService.java | 1 + .../api/spring/service/oidc/OidcActivationContext.java | 1 + .../service/oidc/OidcApplicationConfiguration.java | 6 ++++++ .../rest/api/spring/service/oidc/OidcHandler.java | 9 +++++++++ .../rest/api/spring/service/oidc/OidcTokenClient.java | 6 ++++++ .../rest/api/spring/service/oidc/TokenRequest.java | 5 +++++ .../oidc/OidcApplicationConfigurationServiceTest.java | 4 ++-- 7 files changed, 30 insertions(+), 2 deletions(-) diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java index 0d7ca20a..5699e0e6 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java @@ -463,6 +463,7 @@ private ActivationLayer1Response processOidcActivation(final EncryptionContext e .providerId(identity.get("providerId")) .code(identity.get("code")) .nonce(identity.get("nonce")) + .codeVerifier(identity.get("codeVerifier")) .applicationKey(eciesContext.getApplicationKey()) .build(); diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationContext.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationContext.java index c4e10a64..9601c93f 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationContext.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationContext.java @@ -33,6 +33,7 @@ public class OidcActivationContext { private String code; private String nonce; + private String codeVerifier; private String applicationKey; private String providerId; diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java index c78b20c1..69cb080f 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java @@ -59,4 +59,10 @@ public class OidcApplicationConfiguration { */ private String signatureAlgorithm; + /** + * A hint for the mobile application whether to user PKCE. + * If set to {@code true}, {@code codeVerifier} must be present in identity attributes during create activation step. + */ + private boolean pkceEnabled; + } diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcHandler.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcHandler.java index 399590d7..b6f3b78e 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcHandler.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcHandler.java @@ -23,6 +23,7 @@ import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthActivationException; import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthApplicationConfigurationException; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -73,6 +74,7 @@ public class OidcHandler { */ public String retrieveUserId(final OidcActivationContext request) throws PowerAuthActivationException { final OidcApplicationConfiguration oidcApplicationConfiguration = fetchOidcApplicationConfiguration(request); + validate(request, oidcApplicationConfiguration); final ClientRegistration clientRegistration = createClientRegistration(request.getProviderId(), oidcApplicationConfiguration); @@ -80,6 +82,7 @@ public String retrieveUserId(final OidcActivationContext request) throws PowerAu final TokenRequest tokenRequest = TokenRequest.builder() .code(request.getCode()) + .codeVerifier(request.getCodeVerifier()) .clientRegistration(clientRegistration) .build(); @@ -89,6 +92,12 @@ public String retrieveUserId(final OidcActivationContext request) throws PowerAu return idToken.getSubject(); } + private static void validate(final OidcActivationContext context, final OidcApplicationConfiguration configuration) throws PowerAuthActivationException { + if (configuration.isPkceEnabled() && StringUtils.isBlank(context.getCodeVerifier())) { + throw new PowerAuthActivationException("PKCE is enabled, CodeVerifier must be present."); + } + } + private static ClientRegistration createClientRegistration(final String providerId, final OidcApplicationConfiguration oidcApplicationConfiguration) { logger.debug("Trying to configure via {}/.well-known/openid-configuration", oidcApplicationConfiguration.getIssuerUri()); try { diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcTokenClient.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcTokenClient.java index 9a7fdb61..ef831d91 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcTokenClient.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcTokenClient.java @@ -25,6 +25,7 @@ import com.wultra.core.rest.client.base.RestClientException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -68,6 +69,11 @@ TokenResponse fetchTokenResponse(final TokenRequest tokenRequest) throws RestCli map.add("code", tokenRequest.getCode()); map.add("redirect_uri", clientRegistration.getRedirectUri()); + final String codeVerifier = tokenRequest.getCodeVerifier(); + if (StringUtils.isNotBlank(codeVerifier)) { + map.add("code_verifier", codeVerifier); + } + if (clientAuthenticationMethod == org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_POST) { map.add("client_secret", clientRegistration.getClientSecret()); } diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenRequest.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenRequest.java index b5f6372c..7a4171f3 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenRequest.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenRequest.java @@ -35,4 +35,9 @@ class TokenRequest { private String code; private ClientRegistration clientRegistration; + /** + * Optional. Required only for PKCE. + */ + private String codeVerifier; + } diff --git a/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationServiceTest.java b/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationServiceTest.java index 9b36cd50..1ea156f0 100644 --- a/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationServiceTest.java +++ b/powerauth-restful-security-spring/src/test/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfigurationServiceTest.java @@ -33,8 +33,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; /** @@ -84,6 +83,7 @@ void testFetchOidcApplicationConfiguration() throws Exception { assertEquals("https://token.example.com", result.getTokenUri()); assertEquals("https://authorize.example.com", result.getAuthorizeUri()); assertEquals("ES256", result.getSignatureAlgorithm()); + assertFalse(result.isPkceEnabled()); } @Test From 6037791f5fb8f5a41a58f2b6d614308caee78574 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Tue, 17 Sep 2024 13:31:28 +0200 Subject: [PATCH 12/19] Change log level of DIRECT activation fallback A follow-up to #532 --- .../powerauth/rest/api/spring/service/ActivationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java index 5699e0e6..efbce413 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/ActivationService.java @@ -448,7 +448,7 @@ private ActivationLayer1Response processDirectOrCustomActivation(final Encryptio if (METHOD_OIDC.equals(method)) { return processOidcActivation(eciesContext, request); } else { - logger.info("Unknown method: {} of direct activation, fallback to custom activation", method); + logger.debug("Unknown method: {} of direct activation, fallback to custom activation", method); } } From 1482c5664930210b1ae117bd92ecca3f3e712bb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 07:45:54 +0000 Subject: [PATCH 13/19] Bump org.springframework.boot:spring-boot-dependencies Bumps [org.springframework.boot:spring-boot-dependencies](https://github.com/spring-projects/spring-boot) from 3.3.3 to 3.3.4. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 747e481d..d7101db4 100644 --- a/pom.xml +++ b/pom.xml @@ -84,7 +84,7 @@ 3.10.0 3.3.1 3.4.0 - 3.3.3 + 3.3.4 1.12.0 1.11.0-SNAPSHOT From 24d1f19f6a547b85b0f2ce3a4d3fb0e1a65d69c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:19:45 +0000 Subject: [PATCH 14/19] Bump io.getlime.core:core-bom from 1.11.0-SNAPSHOT to 1.11.0 Bumps [io.getlime.core:core-bom](https://github.com/wultra/lime-java-core) from 1.11.0-SNAPSHOT to 1.11.0. - [Release notes](https://github.com/wultra/lime-java-core/releases) - [Commits](https://github.com/wultra/lime-java-core/commits/1.11.0) --- updated-dependencies: - dependency-name: io.getlime.core:core-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d7101db4..ed24bfe5 100644 --- a/pom.xml +++ b/pom.xml @@ -87,7 +87,7 @@ 3.3.4 1.12.0 - 1.11.0-SNAPSHOT + 1.11.0 1.9.0-SNAPSHOT 1.9.0-SNAPSHOT From 03adf70f71b61b674e1aafabb884b88480f3026c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:19:52 +0000 Subject: [PATCH 15/19] Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.10.0 to 3.10.1 Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.10.0 to 3.10.1. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.10.0...maven-javadoc-plugin-3.10.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d7101db4..dd3e50c9 100644 --- a/pom.xml +++ b/pom.xml @@ -81,7 +81,7 @@ 3.1.3 3.5.0 3.4.2 - 3.10.0 + 3.10.1 3.3.1 3.4.0 3.3.4 From 794763f240371b168bccd0a4213deaeef4616dd8 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Thu, 10 Oct 2024 10:32:41 +0200 Subject: [PATCH 16/19] Fix #561: Set release version to 1.9.0 --- pom.xml | 2 +- powerauth-restful-model/pom.xml | 2 +- powerauth-restful-security-spring-annotation/pom.xml | 2 +- powerauth-restful-security-spring/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index c9ae44af..a979a40d 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ io.getlime.security powerauth-restful-integration-parent - 1.9.0-SNAPSHOT + 1.9.0 pom 2017 diff --git a/powerauth-restful-model/pom.xml b/powerauth-restful-model/pom.xml index fc06e24f..43c50eaf 100644 --- a/powerauth-restful-model/pom.xml +++ b/powerauth-restful-model/pom.xml @@ -30,7 +30,7 @@ io.getlime.security powerauth-restful-integration-parent - 1.9.0-SNAPSHOT + 1.9.0 diff --git a/powerauth-restful-security-spring-annotation/pom.xml b/powerauth-restful-security-spring-annotation/pom.xml index 27ad4347..5b3fce55 100644 --- a/powerauth-restful-security-spring-annotation/pom.xml +++ b/powerauth-restful-security-spring-annotation/pom.xml @@ -30,7 +30,7 @@ io.getlime.security powerauth-restful-integration-parent - 1.9.0-SNAPSHOT + 1.9.0 diff --git a/powerauth-restful-security-spring/pom.xml b/powerauth-restful-security-spring/pom.xml index 016b4a74..1aeed18d 100644 --- a/powerauth-restful-security-spring/pom.xml +++ b/powerauth-restful-security-spring/pom.xml @@ -30,7 +30,7 @@ io.getlime.security powerauth-restful-integration-parent - 1.9.0-SNAPSHOT + 1.9.0 From a58ce6205a13d3576dadb6ba38a07b245e6d4df1 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Thu, 10 Oct 2024 10:40:07 +0200 Subject: [PATCH 17/19] Fix a typo in the JavaDoc A follow-up #532 --- .../api/spring/service/oidc/OidcApplicationConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java index 69cb080f..a65d6326 100644 --- a/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java @@ -38,7 +38,7 @@ public class OidcApplicationConfiguration { private String clientSecret; /** - * Optional. If emtpy, {@code client_secret_basic} is used. + * Optional. If empty, {@code client_secret_basic} is used. */ private ClientAuthenticationMethod clientAuthenticationMethod; From b7214e1790e48d3fd0c48f0cbab10049b193f16c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 07:58:47 +0000 Subject: [PATCH 18/19] Bump powerauth-crypto.version from 1.9.0-SNAPSHOT to 1.9.0 Bumps `powerauth-crypto.version` from 1.9.0-SNAPSHOT to 1.9.0. Updates `io.getlime.security:powerauth-java-crypto` from 1.9.0-SNAPSHOT to 1.9.0 - [Release notes](https://github.com/wultra/powerauth-crypto/releases) - [Changelog](https://github.com/wultra/powerauth-crypto/blob/develop/docs/Releases.md) - [Commits](https://github.com/wultra/powerauth-crypto/commits/1.9.0) Updates `io.getlime.security:powerauth-java-http` from 1.9.0-SNAPSHOT to 1.9.0 - [Release notes](https://github.com/wultra/powerauth-crypto/releases) - [Changelog](https://github.com/wultra/powerauth-crypto/blob/develop/docs/Releases.md) - [Commits](https://github.com/wultra/powerauth-crypto/commits/1.9.0) --- updated-dependencies: - dependency-name: io.getlime.security:powerauth-java-crypto dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.getlime.security:powerauth-java-http dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c9ae44af..13a7bcdf 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ 1.11.0 1.9.0-SNAPSHOT - 1.9.0-SNAPSHOT + 1.9.0 From eceb9c4cf206bf5bb65cb78aaf5cb940aaefe93c Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Thu, 10 Oct 2024 10:31:15 +0200 Subject: [PATCH 19/19] Fix #563: Update Wultra dependencies --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13a7bcdf..d10ccd00 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ 1.12.0 1.11.0 - 1.9.0-SNAPSHOT + 1.9.0 1.9.0