Skip to content

Commit

Permalink
Fix #81: Create endpoint for obtaining a new activation code
Browse files Browse the repository at this point in the history
  • Loading branch information
petrdvorak committed Sep 1, 2021
1 parent 3fbe326 commit 4ae294c
Show file tree
Hide file tree
Showing 12 changed files with 546 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* PowerAuth Enrollment Server
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
package com.wultra.app.enrollmentserver.controller.api;

import com.wultra.app.enrollmentserver.errorhandling.ActivationCodeException;
import com.wultra.app.enrollmentserver.errorhandling.InvalidRequestObjectException;
import com.wultra.app.enrollmentserver.impl.service.ActivationCodeService;
import com.wultra.app.enrollmentserver.model.request.ActivationCodeRequest;
import com.wultra.app.enrollmentserver.model.response.ActivationCodeResponse;
import io.getlime.core.rest.model.base.request.ObjectRequest;
import io.getlime.core.rest.model.base.response.ObjectResponse;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesScope;
import io.getlime.security.powerauth.crypto.lib.enums.PowerAuthSignatureTypes;
import io.getlime.security.powerauth.rest.api.base.authentication.PowerAuthApiAuthentication;
import io.getlime.security.powerauth.rest.api.base.exception.PowerAuthAuthenticationException;
import io.getlime.security.powerauth.rest.api.spring.annotation.EncryptedRequestBody;
import io.getlime.security.powerauth.rest.api.spring.annotation.PowerAuth;
import io.getlime.security.powerauth.rest.api.spring.annotation.PowerAuthEncryption;
import io.swagger.v3.oas.annotations.Parameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
* Controller publishing REST services for obtaining a new activation code.
*
* @author Petr Dvorak, petr@wultra.com
*/
@ConditionalOnProperty(
value = "enrollment-server.activation-spawn.enabled",
havingValue = "true"
)
@RestController
@RequestMapping(value = "api/activation")
public class ActivationCodeController {

private static final Logger logger = LoggerFactory.getLogger(ActivationCodeController.class);

private final ActivationCodeService activationCodeService;

/**
* Default autowiring constructor.
*
* @param activationCodeService Activation code service.
*/
@Autowired
public ActivationCodeController(ActivationCodeService activationCodeService) {
this.activationCodeService = activationCodeService;
}

/**
* Controller request handler for requesting the activation code.
*
* @param request Request with activation OTP.
* @param apiAuthentication Authentication object with user and app details.
* @return New activation code, activation code signature and activation ID.
* @throws PowerAuthAuthenticationException In case user authentication fails.
* @throws InvalidRequestObjectException In case the object validation fails.
* @throws ActivationCodeException In case fetching the activation code fails.
*/
@RequestMapping(value = "code", method = RequestMethod.POST)
@PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE)
@PowerAuth(resourceId = "api/activation/code", signatureType = {
PowerAuthSignatureTypes.POSSESSION_BIOMETRY,
PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE
})
public ObjectResponse<ActivationCodeResponse> requestActivationCode(@EncryptedRequestBody ObjectRequest<ActivationCodeRequest> request, @Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication) throws PowerAuthAuthenticationException, InvalidRequestObjectException, ActivationCodeException {
// Check if the authentication object is present
if (apiAuthentication == null) {
logger.error("Unable to verify device registration when fetching activation code");
throw new PowerAuthAuthenticationException("Unable to verify device registration when fetching activation code");
}

// Request the activation code details.
final ActivationCodeResponse response = activationCodeService.requestActivationCode(request.getRequestObject(), apiAuthentication);
return new ObjectResponse<>(response);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ private Response registerDeviceImpl(

// Check if the context is authenticated - if it is, add activation ID.
// This assures that the activation is assigned with a correct device.
String userId = apiAuthentication.getUserId();
String activationId = apiAuthentication.getActivationId();
Long applicationId = apiAuthentication.getApplicationId();
final String userId = apiAuthentication.getUserId();
final String activationId = apiAuthentication.getActivationId();
final Long applicationId = apiAuthentication.getApplicationId();

return pushRegistrationService.registerDevice(request, userId, activationId, applicationId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* PowerAuth Enrollment Server
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
package com.wultra.app.enrollmentserver.errorhandling;

/**
* Exception thrown in case activation code cannot be fetched for any reason.
*
* @author Petr Dvorak, petr@wultra.com
*/
public class ActivationCodeException extends Exception {
private static final long serialVersionUID = 368452747764248241L;
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,16 @@ public class DefaultExceptionHandler {
return new ErrorResponse(ex.getCode(), ex.getMessage());
}

/**
* Handling of activation code exceptions.
* @param ex Exception.
* @return Response with error details.
*/
@ExceptionHandler(ActivationCodeException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public @ResponseBody ErrorResponse handleActivationCodeException(ActivationCodeException ex) {
logger.warn("Unable to fetch activation code", ex);
return new ErrorResponse("ACTIVATION_CODE_FAILED", "Unable to fetch activation code.");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* PowerAuth Enrollment Server
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
package com.wultra.app.enrollmentserver.impl.service;

import java.util.List;

/**
* Callback provider for lifecycle events of activation code.
*
* @author Petr Dvorak, petr@wultra.com
*/
public interface ActivationCodeDelegate {

/**
* Fetch destination application ID value based in application ID. Check if the source app can
* activate the destination one.
*
* @param applicationId Application identifier for app lookup.
* @param sourceAppId Source application ID.
* @param activationFlags Activation flags.
* @param applicationRoles Application roles.
* @return Destination application ID.
*/
Long destinationApplicationId(String applicationId, Long sourceAppId, List<String> activationFlags, List<String> applicationRoles);

/**
* Callback method to add new activation flags to activation.
*
* @param sourceActivationId Source activation ID (activation used to fetch the code).
* @param sourceActivationFlags Source activation flags (flags of the activation that initiated the transfer).
* @param userId User ID (user ID who requested the activation).
* @param sourceAppId Source app ID (the app that initiated the process).
* @param sourceApplicationRoles Source application roles (roles of the app that intiated the transfer).
* @param destinationAppId Destination app ID (the app that is to be activated).
* @param destinationActivationId Destination activation ID (the activation ID of the new activation).
* @param activationCode Activation code of the new activation.
* @param activationCodeSignature Activation code signature of the new activation code.
* @return List of new activation flags for the destination activation.
*/
List<String> addActivationFlags(String sourceActivationId, List<String> sourceActivationFlags, String userId, Long sourceAppId, List<String> sourceApplicationRoles, Long destinationAppId, String destinationActivationId, String activationCode, String activationCodeSignature);

/**
* Callback method with newly created activation code information.
*
* @param sourceActivationId Source activation ID (activation used to fetch the code).
* @param userId User ID (user ID who requested the activation).
* @param sourceAppId Source app ID (the app that initiated the process).
* @param destinationAppId Destination app ID (the app that is to be activated).
* @param destinationActivationId Destination activation ID (the activation ID of the new activation).
* @param activationCode Activation code of the new activation.
* @param activationCodeSignature Activation code signature of the new activation code.
*/
void didReturnActivationCode(String sourceActivationId, String userId, Long sourceAppId, Long destinationAppId, String destinationActivationId, String activationCode, String activationCodeSignature);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* PowerAuth Enrollment Server
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
package com.wultra.app.enrollmentserver.impl.service;

import com.wultra.app.enrollmentserver.errorhandling.ActivationCodeException;
import com.wultra.app.enrollmentserver.errorhandling.InvalidRequestObjectException;
import com.wultra.app.enrollmentserver.impl.service.converter.ActivationCodeConverter;
import com.wultra.app.enrollmentserver.model.request.ActivationCodeRequest;
import com.wultra.app.enrollmentserver.model.response.ActivationCodeResponse;
import com.wultra.app.enrollmentserver.model.validator.ActivationCodeRequestValidator;
import com.wultra.security.powerauth.client.PowerAuthClient;
import com.wultra.security.powerauth.client.model.error.PowerAuthClientException;
import com.wultra.security.powerauth.client.v3.ActivationOtpValidation;
import com.wultra.security.powerauth.client.v3.InitActivationResponse;
import io.getlime.security.powerauth.rest.api.base.authentication.PowerAuthApiAuthentication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* Service responsible for fetching the new activation codes.
*
* @author Petr Dvorak, petr@wultra.com
*/
@Service
public class ActivationCodeService {

private static final Logger logger = LoggerFactory.getLogger(ActivationCodeService.class);

private final PowerAuthClient powerAuthClient;
private final ActivationCodeConverter activationCodeConverter;

private ActivationCodeDelegate activationCodeDelegate;

/**
* Autowiring constructor.
*
* @param powerAuthClient PowerAuth Client instance.
* @param activationCodeConverter Activation code converter class.
*/
@Autowired
public ActivationCodeService(PowerAuthClient powerAuthClient, ActivationCodeConverter activationCodeConverter) {
this.powerAuthClient = powerAuthClient;
this.activationCodeConverter = activationCodeConverter;
}

/**
* Set activation code delegate via auto-wiring.
*
* @param activationCodeDelegate Activation code delegate bean.
*/
@Autowired(required = false)
public void setActivationCodeDelegate(ActivationCodeDelegate activationCodeDelegate) {
this.activationCodeDelegate = activationCodeDelegate;
}

/**
* Request activation code for provided OTP value, user ID and app ID.
*
* @param request Request with OTP value.
* @param apiAuthentication Authentication object.
* @return Response with activation code, activation code signature, and activation ID.
* @throws ActivationCodeException In case of invalid user / app attributes or communication with PowerAuth Service fails.
* @throws InvalidRequestObjectException In case request object validation fails.
*/
public ActivationCodeResponse requestActivationCode(ActivationCodeRequest request, PowerAuthApiAuthentication apiAuthentication) throws InvalidRequestObjectException, ActivationCodeException {

// Fetch information from the authentication object
final String sourceActivationId = apiAuthentication.getActivationId();
final String sourceUserId = apiAuthentication.getUserId();
final Long sourceAppId = apiAuthentication.getApplicationId();
final List<String> sourceActivationFlags = apiAuthentication.getActivationFlags();
final List<String> sourceApplicationRoles = apiAuthentication.getApplicationRoles();

logger.info("Activation code registration started, user ID: {}", sourceUserId);

// Verify that activation code delegate is implemented
if (activationCodeDelegate == null) {
logger.error("Missing activation code implementation");
throw new ActivationCodeException();
}

// Validate the request object
final String error = ActivationCodeRequestValidator.validate(request);
if (error != null) {
logger.error("Invalid object in activation code request - {}, user ID: {}", error, sourceUserId);
throw new InvalidRequestObjectException();
}

// Get the request parameters
final String otp = request.getOtp();
final String applicationId = request.getApplicationId();

final Long destinationAppId = activationCodeDelegate.destinationApplicationId(applicationId, sourceAppId, sourceActivationFlags, sourceApplicationRoles);
if (destinationAppId == null) {
logger.error("Invalid application ID. The provided source app ID: {} cannot activate the destination app ID: {}.", sourceAppId, applicationId);
throw new ActivationCodeException();
}

try {
// Create a new activation
logger.info("Calling PowerAuth Server with new activation request, user ID: {}, app ID: {}, otp: {}, ", sourceUserId, destinationAppId, otp);
final InitActivationResponse iar = powerAuthClient.initActivation(
sourceUserId, destinationAppId, ActivationOtpValidation.ON_KEY_EXCHANGE, otp
);
logger.info("Successfully obtained a new activation with ID: {}", iar.getActivationId());

// Notify systems about newly created activation
activationCodeDelegate.didReturnActivationCode(
sourceActivationId, sourceUserId, sourceAppId, destinationAppId,
iar.getActivationId(), iar.getActivationCode(), iar.getActivationSignature()
);

// Add the activation flags
final List<String> flags = activationCodeDelegate.addActivationFlags(
sourceActivationId, sourceActivationFlags, sourceUserId, sourceAppId, sourceApplicationRoles, destinationAppId,
iar.getActivationId(), iar.getActivationCode(), iar.getActivationSignature()
);
if (flags != null && !flags.isEmpty()) {
logger.info("Calling PowerAuth Server to add activation flags to activation ID: {}, flags: {}.", iar.getActivationId(), flags.toArray());
powerAuthClient.addActivationFlags(iar.getActivationId(), flags);
logger.info("Successfully added flags to activation ID: {}.", iar.getActivationId());
} else {
logger.info("Activation with ID: {} has no additional flags.", iar.getActivationId());
}

return activationCodeConverter.convert(iar);
} catch (PowerAuthClientException e) {
logger.error("Unable to call upstream service.", e);
throw new ActivationCodeException();
}

}

}
Loading

0 comments on commit 4ae294c

Please sign in to comment.