Skip to content

Commit

Permalink
Fix #1596: Update Callback Management API (#1679)
Browse files Browse the repository at this point in the history
  • Loading branch information
jnpsk authored Sep 30, 2024
1 parent 504efaf commit ece60e0
Show file tree
Hide file tree
Showing 13 changed files with 150 additions and 15 deletions.
11 changes: 11 additions & 0 deletions docs/PowerAuth-Server-1.9.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ The parameter `activationOtpValidation` is deprecated.
Use the `activationOtp` parameter during activation init or activation commit to control the OTP check.
Use the `commitPhase` parameter for specifying when the activation should be committed.

### New Attributes in Callback URL Management API

The Callback URL Management API supports configuration of retry policy and retention period for each Callback URL
configuration. These changes impact both the request and response bodies of `/rest/v3/application/callback/create`
and `/rest/v3/application/callback/update` endpoints and response body of `/rest/v3/application/callback/list` endpoint.

Following attributes have been added:
- `retentionPeriod` defines the duration after which a completed callback event is automatically removed from database.
- `initialBackoff` defines the initial delay before retry attempt following a callback event failure, if retries are enabled.
- `maxAttempts` defines the maximum number of attempts to send a callback event.

### ECDSA Signature Verification in JOSE Format

The method `POST /rest/v3/signature/ecdsa/verify` now supports validation of ECDSA signature in JOSE format, thanks to added `signatureFormat` request attribute (`DER` as a default value, or `JOSE`).
Expand Down
15 changes: 15 additions & 0 deletions docs/WebServices-Methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,9 @@ REST endpoint: `POST /rest/v3/application/callback/create`
| `String` | `callbackUrl` | Callback URL that should be notified about activation status updates. |
| `List<String>` | `attributes` | Attributes which should be sent with the callback. See possible attributes bellow. |
| `String` | `authentication` | Callback HTTP request authentication configuration. |
| `Duration` | `retentionPeriod` | Duration in ISO 8601 duration format after which a completed callback event is automatically removed from database. |
| `Duration` | `initialBackoff` | Initial delay in ISO 8601 duration format before retry attempt following a callback event failure, if retries are enabled. |
| `Integer` | `maxAttempts` | Maximum number of attempts to send a callback event. |

When creating a callback URL of type `ACTIVATION_STATUS_CHANGE`, following `attributes` can be used:

Expand Down Expand Up @@ -1466,6 +1469,9 @@ The `authentication` parameter contains a JSON-based configuration for client TL
| `String` | `callbackUrl` | Callback URL that should be notified about activation status updates. |
| `List<String>` | `attributes` | Attributes which should be sent with the callback. |
| `String` | `authentication` | Callback HTTP request authentication configuration. |
| `Duration` | `retentionPeriod` | Duration in ISO 8601 duration format after which a completed callback event is automatically removed from database. |
| `Duration` | `initialBackoff` | Initial delay in ISO 8601 duration format before retry attempt following a callback event failure, if retries are enabled. |
| `Integer` | `maxAttempts` | Maximum number of attempts to send a callback event. |

### Method 'updateCallbackUrl'

Expand All @@ -1486,6 +1492,9 @@ REST endpoint: `POST /rest/v3/application/callback/update`
| `String` | `callbackUrl` | Callback URL that should be notified about activation status updates. |
| `List<String>` | `attributes` | Attributes which should be sent with the callback. See possible attributes bellow. |
| `String` | `authentication` | Callback HTTP request authentication configuration. |
| `Duration` | `retentionPeriod` | Duration in ISO 8601 duration format after which a completed callback event is automatically removed from database. |
| `Duration` | `initialBackoff` | Initial delay in ISO 8601 duration format before retry attempt following a callback event failure, if retries are enabled. |
| `Integer` | `maxAttempts` | Maximum number of attempts to send a callback event. |

When configuring a callback URL of type `ACTIVATION_STATUS_CHANGE`, following `attributes` can be used:

Expand Down Expand Up @@ -1560,6 +1569,9 @@ The `authentication` parameter contains a JSON-based configuration for client TL
| `String` | `callbackUrl` | Callback URL that should be notified about activation status updates. |
| `List<String>` | `attributes` | Attributes which should be sent with the callback. |
| `String` | `authentication` | Callback HTTP request authentication configuration. |
| `Duration` | `retentionPeriod` | Duration in ISO 8601 duration format after which a completed callback event is automatically removed from database. |
| `Duration` | `initialBackoff` | Initial delay in ISO 8601 duration format before retry attempt following a callback event failure, if retries are enabled. |
| `Integer` | `maxAttempts` | Maximum number of attempts to send a callback event. |

### Method 'getCallbackUrlList'

Expand Down Expand Up @@ -1594,6 +1606,9 @@ REST endpoint: `POST /rest/v3/application/callback/list`
| `String` | `callbackUrl` | Callback URL that should be notified about activation status updates. |
| `List<String>` | `attributes` | Attributes which should be sent with the callback. |
| `String` | `authentication` | Callback HTTP request authentication configuration. |
| `Duration` | `retentionPeriod` | Duration in ISO 8601 duration format after which a completed callback event is automatically removed from database. |
| `Duration` | `initialBackoff` | Initial delay in ISO 8601 duration format before retry attempt following a callback event failure, if retries are enabled. |
| `Integer` | `maxAttempts` | Maximum number of attempts to send a callback event. |

### Method 'removeCallbackUrl'

Expand Down
4 changes: 4 additions & 0 deletions powerauth-client-model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
*/
package com.wultra.security.powerauth.client.model.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -38,4 +41,15 @@ public class CallbackUrl {
private List<String> attributes = new ArrayList<>();
private HttpAuthenticationPublic authentication = new HttpAuthenticationPublic();

@JsonFormat(shape = JsonFormat.Shape.STRING)
@Schema(type = "string", format = "ISO 8601 Duration", example = "P30D")
private Duration retentionPeriod;

@JsonFormat(shape = JsonFormat.Shape.STRING)
@Schema(type = "string", format = "ISO 8601 Duration", example = "PT2.5S")
private Duration initialBackoff;

@Schema(type = "integer", example = "1")
private Integer maxAttempts;

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@
package com.wultra.security.powerauth.client.model.request;

import com.wultra.security.powerauth.client.model.entity.HttpAuthenticationPrivate;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.hibernate.validator.constraints.time.DurationMin;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -32,11 +37,31 @@
@Data
public class CreateCallbackUrlRequest {

@NotBlank
private String applicationId;

@NotBlank
private String name;

@NotBlank
private String type;

@NotBlank
private String callbackUrl;

private List<String> attributes = new ArrayList<>();
private HttpAuthenticationPrivate authentication = new HttpAuthenticationPrivate();

@DurationMin(message = "Duration must be positive or zero")
@Schema(type = "string", format = "ISO 8601 Duration", example = "P30D")
private Duration retentionPeriod;

@DurationMin(message = "Duration must be positive or zero")
@Schema(type = "string", format = "ISO 8601 Duration", example = "PT2.5S")
private Duration initialBackoff;

@Min(1)
@Schema(type = "integer", example = "1")
private Integer maxAttempts;

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@
package com.wultra.security.powerauth.client.model.request;

import com.wultra.security.powerauth.client.model.entity.HttpAuthenticationPrivate;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.hibernate.validator.constraints.time.DurationMin;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -32,12 +37,34 @@
@Data
public class UpdateCallbackUrlRequest {

@NotBlank
private String id;

@NotBlank
private String applicationId;

@NotBlank
private String name;

@NotBlank
private String type;

@NotBlank
private String callbackUrl;

private List<String> attributes = new ArrayList<>();
private HttpAuthenticationPrivate authentication = new HttpAuthenticationPrivate();

@DurationMin(message = "Duration must be positive or zero")
@Schema(type = "string", format = "ISO 8601 Duration", example = "P30D")
private Duration retentionPeriod;

@DurationMin(message = "Duration must be positive or zero")
@Schema(type = "string", format = "ISO 8601 Duration", example = "PT2.5S")
private Duration initialBackoff;

@Min(1)
@Schema(type = "integer", example = "1")
private Integer maxAttempts;

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@

package com.wultra.security.powerauth.client.model.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.wultra.security.powerauth.client.model.entity.HttpAuthenticationPublic;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -39,4 +42,15 @@ public class CreateCallbackUrlResponse {
private List<String> attributes = new ArrayList<>();
private HttpAuthenticationPublic authentication = new HttpAuthenticationPublic();

@JsonFormat(shape = JsonFormat.Shape.STRING)
@Schema(type = "string", format = "ISO 8601 Duration", example = "P30D")
private Duration retentionPeriod;

@JsonFormat(shape = JsonFormat.Shape.STRING)
@Schema(type = "string", format = "ISO 8601 Duration", example = "PT2.5S")
private Duration initialBackoff;

@Schema(type = "integer", example = "1")
private Integer maxAttempts;

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@

package com.wultra.security.powerauth.client.model.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.wultra.security.powerauth.client.model.entity.HttpAuthenticationPublic;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import lombok.Data;
import org.hibernate.validator.constraints.time.DurationMin;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -40,4 +45,15 @@ public class UpdateCallbackUrlResponse {
private List<String> attributes = new ArrayList<>();
private HttpAuthenticationPublic authentication = new HttpAuthenticationPublic();

@JsonFormat(shape = JsonFormat.Shape.STRING)
@Schema(type = "string", format = "ISO 8601 Duration", example = "P30D")
private Duration retentionPeriod;

@JsonFormat(shape = JsonFormat.Shape.STRING)
@Schema(type = "string", format = "ISO 8601 Duration", example = "PT2.5S")
private Duration initialBackoff;

@Schema(type = "integer", example = "1")
private Integer maxAttempts;

}
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public class RESTControllerAdvice {
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public @ResponseBody ObjectResponse<PowerAuthError> returnActivationRecoveryError(final MethodArgumentNotValidException ex) {
public @ResponseBody ObjectResponse<PowerAuthError> returnInvalidRequestError(final MethodArgumentNotValidException ex) {
logger.error("Error occurred while processing the request: {}", ex.getMessage());
logger.debug("Exception details:", ex);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.getlime.core.rest.model.base.response.ObjectResponse;
import io.getlime.security.powerauth.app.server.service.behavior.tasks.CallbackUrlBehavior;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
Expand Down Expand Up @@ -63,7 +64,7 @@ public ApplicationCallbackController(CallbackUrlBehavior service) {
* @throws Exception In case the service throws exception.
*/
@PostMapping("/create")
public ObjectResponse<CreateCallbackUrlResponse> createCallbackUrl(@RequestBody ObjectRequest<CreateCallbackUrlRequest> request) throws Exception {
public ObjectResponse<CreateCallbackUrlResponse> createCallbackUrl(@Valid @RequestBody ObjectRequest<CreateCallbackUrlRequest> request) throws Exception {
logger.info("CreateCallbackUrlRequest received: {}", request);
final ObjectResponse<CreateCallbackUrlResponse> response = new ObjectResponse<>(service.createCallbackUrl(request.getRequestObject()));
logger.info("CreateCallbackUrlRequest succeeded: {}", response);
Expand All @@ -78,7 +79,7 @@ public ObjectResponse<CreateCallbackUrlResponse> createCallbackUrl(@RequestBody
* @throws Exception In case the service throws exception.
*/
@PostMapping("/update")
public ObjectResponse<UpdateCallbackUrlResponse> updateCallbackUrl(@RequestBody ObjectRequest<UpdateCallbackUrlRequest> request) throws Exception {
public ObjectResponse<UpdateCallbackUrlResponse> updateCallbackUrl(@Valid @RequestBody ObjectRequest<UpdateCallbackUrlRequest> request) throws Exception {
logger.info("UpdateCallbackUrlRequest received: {}", request);
final ObjectResponse<UpdateCallbackUrlResponse> response = new ObjectResponse<>(service.updateCallbackUrl(request.getRequestObject()));
logger.info("UpdateCallbackUrlRequest succeeded: {}", response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,6 @@ public class CallbackUrlBehavior {
@Transactional
public CreateCallbackUrlResponse createCallbackUrl(CreateCallbackUrlRequest request) throws GenericServiceException {
try {
if (request.getName() == null) {
logger.warn("Invalid request parameter name in method createCallbackUrl");
// Rollback is not required, error occurs before writing to database
throw localizationProvider.buildExceptionForCode(ServiceError.INVALID_REQUEST);
}

// Check the URL format
try {
new URL(request.getCallbackUrl());
Expand Down Expand Up @@ -116,7 +110,11 @@ public CreateCallbackUrlResponse createCallbackUrl(CreateCallbackUrlRequest requ
final EncryptableString encrypted = callbackUrlAuthenticationCryptor.encrypt(request.getAuthentication(), entity.getApplication().getId());
entity.setAuthentication(encrypted.encryptedData());
entity.setEncryptionMode(encrypted.encryptionMode());
entity.setRetentionPeriod(request.getRetentionPeriod());
entity.setInitialBackoff(request.getInitialBackoff());
entity.setMaxAttempts(request.getMaxAttempts());
callbackUrlRepository.save(entity);

final CreateCallbackUrlResponse response = new CreateCallbackUrlResponse();
response.setId(entity.getId());
response.setApplicationId(entity.getApplication().getId());
Expand All @@ -126,6 +124,9 @@ public CreateCallbackUrlResponse createCallbackUrl(CreateCallbackUrlRequest requ
response.getAttributes().addAll(entity.getAttributes());
}
response.setAuthentication(callbackUrlAuthenticationCryptor.decryptToPublic(entity));
response.setRetentionPeriod(entity.getRetentionPeriod());
response.setInitialBackoff(entity.getInitialBackoff());
response.setMaxAttempts(entity.getMaxAttempts());
return response;
} catch (GenericServiceException ex) {
// already logged
Expand All @@ -147,12 +148,6 @@ public CreateCallbackUrlResponse createCallbackUrl(CreateCallbackUrlRequest requ
*/
public UpdateCallbackUrlResponse updateCallbackUrl(UpdateCallbackUrlRequest request) throws GenericServiceException {
try {
if (request.getId() == null || request.getApplicationId() == null || request.getName() == null || request.getAttributes() == null) {
logger.warn("Invalid request in method updateCallbackUrl");
// Rollback is not required, error occurs before writing to database
throw localizationProvider.buildExceptionForCode(ServiceError.INVALID_REQUEST);
}

final CallbackUrlEntity entity = callbackUrlRepository.findById(request.getId())
.filter(it -> it.getApplication().getId().equals(request.getApplicationId()))
.orElseThrow(() -> {
Expand All @@ -175,6 +170,7 @@ public UpdateCallbackUrlResponse updateCallbackUrl(UpdateCallbackUrlRequest requ
entity.setName(request.getName());
entity.setCallbackUrl(request.getCallbackUrl());
entity.setAttributes(request.getAttributes());
entity.setType(CallbackUrlType.valueOf(request.getType()));
// Retain existing passwords in case new password is not set
final HttpAuthenticationPrivate authRequest = request.getAuthentication();
final CallbackUrlAuthentication authExisting = callbackUrlAuthenticationCryptor.decrypt(entity);
Expand Down Expand Up @@ -202,6 +198,9 @@ public UpdateCallbackUrlResponse updateCallbackUrl(UpdateCallbackUrlRequest requ
final EncryptableString encrypted = callbackUrlAuthenticationCryptor.encrypt(authRequest, entity.getApplication().getId());
entity.setAuthentication(encrypted.encryptedData());
entity.setEncryptionMode(encrypted.encryptionMode());
entity.setRetentionPeriod(request.getRetentionPeriod());
entity.setInitialBackoff(request.getInitialBackoff());
entity.setMaxAttempts(request.getMaxAttempts());
callbackUrlRepository.save(entity);

final UpdateCallbackUrlResponse response = new UpdateCallbackUrlResponse();
Expand All @@ -214,6 +213,9 @@ public UpdateCallbackUrlResponse updateCallbackUrl(UpdateCallbackUrlRequest requ
response.getAttributes().addAll(entity.getAttributes());
}
response.setAuthentication(callbackUrlAuthenticationCryptor.decryptToPublic(entity));
response.setRetentionPeriod(entity.getRetentionPeriod());
response.setInitialBackoff(entity.getInitialBackoff());
response.setMaxAttempts(entity.getMaxAttempts());
return response;
} catch (GenericServiceException ex) {
// already logged
Expand Down Expand Up @@ -248,6 +250,9 @@ public GetCallbackUrlListResponse getCallbackUrlList(GetCallbackUrlListRequest r
item.getAttributes().addAll(callbackUrl.getAttributes());
}
item.setAuthentication(callbackUrlAuthenticationCryptor.decryptToPublic(callbackUrl));
item.setRetentionPeriod(callbackUrl.getRetentionPeriod());
item.setInitialBackoff(callbackUrl.getInitialBackoff());
item.setMaxAttempts(callbackUrl.getMaxAttempts());
response.getCallbackUrlList().add(item);
}
return response;
Expand Down
Loading

0 comments on commit ece60e0

Please sign in to comment.