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/pom.xml b/pom.xml index 4e68bbcd..e0288979 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ io.getlime.security powerauth-restful-integration-parent - 1.8.0 + 1.9.0 pom 2017 @@ -78,18 +78,18 @@ 17 ${java.version} ${java.version} - 3.1.2 + 3.1.3 3.5.0 3.4.2 - 3.8.0 + 3.10.1 3.3.1 3.4.0 - 3.3.2 + 3.3.4 1.12.0 - 1.10.0 - 1.8.0 - 1.8.0 + 1.11.0 + 1.9.0 + 1.9.0 @@ -131,6 +131,14 @@ lombok provided + + + + io.netty + netty-resolver-dns-native-macos + runtime + osx-aarch_64 + diff --git a/powerauth-restful-model/pom.xml b/powerauth-restful-model/pom.xml index 390bd8a4..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.8.0 + 1.9.0 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-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/pom.xml b/powerauth-restful-security-spring-annotation/pom.xml index 8fb06e25..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.8.0 + 1.9.0 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/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-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/pom.xml b/powerauth-restful-security-spring/pom.xml index 8da451c8..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.8.0 + 1.9.0 @@ -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/controller/ActivationController.java b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/controller/ActivationController.java index 19613619..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,13 +60,16 @@ *

PowerAuth protocol versions: *

    *
  • 3.0
  • + *
  • 3.1
  • + *
  • 3.2
  • + *
  • 3.3
  • *
* * @author Roman Strobl, roman.strobl@wultra.com * */ @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/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 0e97c72c..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,13 +44,16 @@ *

PowerAuth protocol versions: *

    *
  • 3.0
  • + *
  • 3.1
  • + *
  • 3.2
  • + *
  • 3.3
  • *
* * @author Roman Strobl, roman.strobl@wultra.com * */ @RestController -@RequestMapping(value = "/pa/v3/recovery") +@RequestMapping("/pa/v3/recovery") public class RecoveryController { private static final Logger logger = LoggerFactory.getLogger(RecoveryController.class); @@ -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 875c6cc6..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,12 +44,15 @@ *

PowerAuth protocol versions: *

    *
  • 3.0
  • + *
  • 3.1
  • + *
  • 3.2
  • + *
  • 3.3
  • *
* * @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); @@ -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 c54a8645..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,13 +36,16 @@ *

    PowerAuth protocol versions: *

      *
    • 3.0
    • + *
    • 3.1
    • + *
    • 3.2
    • + *
    • 3.3
    • *
    * * @author Roman Strobl, roman.strobl@wultra.com * */ @RestController("signatureControllerV3") -@RequestMapping(value = "/pa/v3/signature") +@RequestMapping("/pa/v3/signature") public class SignatureController { /** 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/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/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 9613d9ac..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 @@ -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; @@ -35,15 +38,16 @@ 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 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; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.time.Instant; @@ -60,13 +64,15 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Service("activationServiceV3") +@Slf4j public class ActivationService { - private static final Logger logger = LoggerFactory.getLogger(ActivationService.class); + 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; @@ -80,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; } /** @@ -123,365 +135,403 @@ public void setUserInfoProvider(UserInfoProvider userInfoProvider) { * @throws PowerAuthRecoveryException In case activation recovery fails. */ public ActivationLayer1Response createActivation(ActivationLayer1Request request, EncryptionContext eciesContext) throws PowerAuthActivationException, PowerAuthRecoveryException { - try { + final ActivationType type = request.getType(); + logger.debug("Handling activation type: {}", type); + final Map identity = request.getIdentityAttributes(); + + checkIdentityAttributesPresent(identity); - final String applicationKey = eciesContext.getApplicationKey(); - final EciesEncryptedRequest activationData = request.getActivationData(); - 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 identity = request.getIdentityAttributes(); - final Map customAttributes = (request.getCustomAttributes() != null) ? request.getCustomAttributes() : new HashMap<>(); - - switch (request.getType()) { + try { + return switch (type) { // 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"); - - 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.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(); - - // 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); - } - } - - 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 CommitActivationRequest commitRequest = new CommitActivationRequest(); - commitRequest.setActivationId(activationId); - commitRequest.setExternalUserId(null); - final CommitActivationResponse commitResponse = powerAuthClient.commitActivation( - commitRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - - 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(); - } - - // 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<>(); - - // 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.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(); - - // 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); - } - } - - // 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 CommitActivationRequest commitRequest = new CommitActivationRequest(); - commitRequest.setActivationId(activationId); - commitRequest.setExternalUserId(null); - final CommitActivationResponse commitResponse = powerAuthClient.commitActivation( - commitRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - 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 -> { - - // 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"); - - 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.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(); - - // 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); - } - } - - 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 CommitActivationRequest commitRequest = new CommitActivationRequest(); - commitRequest.setActivationId(activationId); - commitRequest.setExternalUserId(null); - final CommitActivationResponse commitResponse = powerAuthClient.commitActivation( - commitRequest, - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - 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 -> { - logger.warn("Invalid activation request"); - throw new PowerAuthInvalidRequestException(); - } - } + case CODE -> processCodeActivation(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) { 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("Creating PowerAuth activation failed.", ex); + } + } + + 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 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.debug("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")) + .codeVerifier(identity.get("codeVerifier")) + .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."); + } + } + + 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 { @@ -641,13 +691,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(); 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/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..9601c93f --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcActivationContext.java @@ -0,0 +1,40 @@ +/* + * 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 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 new file mode 100644 index 00000000..a65d6326 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcApplicationConfiguration.java @@ -0,0 +1,68 @@ +/* + * 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 empty, {@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; + + /** + * 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/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..b6f3b78e --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcHandler.java @@ -0,0 +1,182 @@ +/* + * 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.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; +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); + validate(request, oidcApplicationConfiguration); + + final ClientRegistration clientRegistration = createClientRegistration(request.getProviderId(), oidcApplicationConfiguration); + + signatureAlgorithms.putIfAbsent(clientRegistration.getRegistrationId(), mapSignatureAlgorithmFromConfiguration(oidcApplicationConfiguration)); + + final TokenRequest tokenRequest = TokenRequest.builder() + .code(request.getCode()) + .codeVerifier(request.getCodeVerifier()) + .clientRegistration(clientRegistration) + .build(); + + final TokenResponse tokenResponse = fetchToken(tokenRequest); + final Jwt idToken = verifyAndDecode(tokenResponse, clientRegistration, request.getNonce()); + + 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 { + 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..ef831d91 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/OidcTokenClient.java @@ -0,0 +1,104 @@ +/* + * 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.apache.commons.lang3.StringUtils; +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()); + + 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()); + } + + 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..7a4171f3 --- /dev/null +++ b/powerauth-restful-security-spring/src/main/java/io/getlime/security/powerauth/rest/api/spring/service/oidc/TokenRequest.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 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; + + /** + * Optional. Required only for PKCE. + */ + private String codeVerifier; + +} 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/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 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..1ea156f0 --- /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.*; +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()); + assertFalse(result.isPkceEnabled()); + } + + @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